LCT 学习笔记
文艺平衡树
LCT 的前置知识当然时文艺平衡树啦。先讲讲文艺平衡树。
题意
给你一个长度为 \(n\) 的序列 \(a_i\),\(T\) 次操作,每次翻转一个区间,输出最后的序列。
题解
维护一个 Splay,一开始按照 \(a_i\) 为下标,\(i\) 为键值 进行建树,可以理解成键值就是这个点的深度。
对于区间翻转 \([l,r]\),我们先将键值为 \(l-1\) 的点旋至根,再将键值为 \(r+1\) 的点旋至 \(l-1\) 的右儿子,这样由于平衡树左大右小的性质, \(r+1\) 点的子树就都是原序列 \([l,r]\) 内的节点了。
因为翻转后,最左的点与最右的点交换,第二左的点与第二右的点交换……也就是第 \(i\) 左的点与第 \(i\) 右的点交换,也就是第 \(i\) 浅的点与第 \(i\) 深的点交换。
所以,我们对这个子树每一个左右节点进行反转。这就相当于第 \(i\) 浅的点与第 \(i\) 深的点交换了。
注意:我们不会真的去更新键值,而是依靠一个点在平衡树中的位置去判断它在原序列哪一个位置。讨论
进行反转时间复杂度太高,所以打 LazyTag,像线段树一样,当访问到此节点时再执行反转操作并下推标记即可。
LCT 简介
略。
注意虚边连接的平衡树,代表着 这棵平衡树深度最小的点连接着上面的点。
下文所说一个点的根指的是它所在连通块中,深度最小的点。
LCT 基操
Splay
Splay(o)
支持将点 \(o\) 旋转至它的平衡树的根。注意虚边是认父不认子的,所以根可能也有父亲。但是这父亲可不能连边,所以要特判。
void updat(int o){tre[o].tot=tre[lsn(o)].tot^tre[rsn(o)].tot^tre[o].val;}
bool Dwhi(int o){return rsn(tre[o].fa)==o;}
bool isrot(int o){return lsn(tre[o].fa)^o&&rsn(tre[o].fa)^o;}
void rota(int o){
int y=tre[o].fa,z=tre[y].fa,whi=Dwhi(o);
int fawhi=( isrot(y)?-1:Dwhi(y) ),v=tre[o].son[whi^1];
tre[v].fa=y,tre[y].son[whi]=v;
tre[y].fa=o,tre[o].son[whi^1]=y;
tre[o].fa=z;if(~fawhi)tre[z].son[fawhi]=o;
updat(y),updat(o);
}
void Splay(int o){
puall(o);
while( !isrot(o) ){
int y=tre[o].fa;
if( !isrot(y) ){
Dwhi(o)==Dwhi(y)?rota(y):rota(o);
}
rota(o);
}
}
access
access(o)
是 LCT 的核心操作:将点 \(o\) 到原树中 \(o\) 所在连通块的根的路径打包成一个实链。(当然,原来的某些实边可能会因此变成虚边)
首先将 \(o\) 旋转到所处平衡树的根,然后将它的右儿子设为 \(0\)(因为 \(o\) 到根的路径必定是越来越浅的,但是右儿子是深度比它大的,所以砍掉)。更新 \(o\) 的信息,然后设 \(las=o\),跳上 \(o\) 的父亲(即虚边上面的点)。
再次按照上面操作,但是将 \(o\) 的右儿子设为 \(las\)。直到 \(o\) 没有父亲为止。
void aces(int o){
int las=0;
while(o){
Splay(o),rsn(o)=las,updat(o);
las=o,o=tre[o].fa;
}
}
有了 access
,我们就可以做很多很多操作了。
makeroot
makeroot(o)
将 \(o\) 设为原树中 \(o\) 所在的连通块的根。
这就相当于将先 access(o)
,将 \(o\) 和根的路径打通,然后反转路径上的所有父子关系。也就是第一深和第一浅交换,第二深与第二浅交换……
于是文艺平衡树的翻转标记来啦,Splay(o)
将 \(o\) 置为当前平衡树的根,然后打个翻转标记。
void Mroot(int o){aces(o),Splay(o),tre[o].laz^=1;}
findroot
findroot(o)
找到原树中 \(o\) 所在的连通块的根。
这就相当于找到连通块中深度最浅的点。
将 \(o\) 和根的路径打通,将 \(o\) 旋转至平衡树根,然后不断走向它的左儿子(左儿子深度浅)。
最后再 Splay(o)
,使平衡树保持平衡。
int Froot(int o){
aces(o),Splay(o);
while( lsn(o) )pudown(o),o=lsn(o);
Splay(o);
return o;
}
split
split(o1,o2)
将 \(o1\sim o2\) 的路径打包成一个实链,并将 \(o2\) 设为平衡树的根,前提是 \(o1,o2\) 在一个连通块中。
makeroot(o1),access(o2)
即可,最后 Splay(o2)
使平衡树保持平衡。
void split(int o1,int o2){Mroot(o1),aces(o2),Splay(o2);}
link
link(o1,o2)
在原树上连边 o1,o2
(如果两点已经在一个连通块,则不操作)。
直接将 o1
设为当前连通块的根,然后他的父亲变成 o2
即可(其中还要判断下 o2
的根是不是 o1
)。
o2
不需要认 o1
为儿子,因为这条新边默认为虚边,认父不认子。
void link(int o1,int o2){
Mroot(o1);
if(Froot(o2)==o1)return;
tre[o1].fa=o2;
}
cut
cut(o1,o2)
在原树上删边 o1,o2
(如果没有这条边,则不操作)。
这个操作判合法比较难。
首先如果 o1,o2
不在同一个连通块,那么显然不操作。
然后就把 o1,o2
放在一个 Splay 中(split
)。
此时 o1
就是原树上连通块的根,o2
就是平衡树的根了,如果 o1
和 o2
之间连边,那么它们的深度必定相差 \(1\)。这意味着 o1
是 o2
的左儿子(因为 o1
是根,最浅)。如果不是,那么不操作。
而且因为 o1
是一个实链的根,所以它在这个实链中只会与一个点连边(就是 o2
),故 o1
理应没有儿子。所以再判断 o1
有没有任何儿子,如果有儿子就不操作。
最后经过重重判合法,我们直接让 o1,o2
断绝一切关系(o1
没有父亲、 o2
没有左儿子),并更新 o2
的信息。
void cut(int o1,int o2){
if( Froot(o1)^Froot(o2) )return;
split(o1,o2);
if(tre[o1].fa^o2|| lsn(o1) || rsn(o1) )return;
tre[o1].fa=lsn(o2)=0;
updat(o2);
}
以上所有的操作时间复杂度均为 \(O(\log n)\),所以总共为 \(O(\ (n+T)\log n\ )\)。
例题
例题 及其代码:(都是上面的操作,就不讲了,记得一开始的建边也是 link
)
#include<bits/stdc++.h>
#define rep(i,x,y) for(int i=x;i<=y;++i)
#define lsn(o) tre[o].son[0]
#define rsn(o) tre[o].son[1]
using namespace std;
const int n7=101234;
struct mist{int val,tot,siz,fa,son[2];bool laz;}tre[n7];
int n,T,a[n7];
int rd(){
int shu=0;bool fu=0;char ch=getchar();
while( !isdigit(ch) ){if(ch=='-')fu=1;ch=getchar();}
while( isdigit(ch) )shu=(shu<<1)+(shu<<3)+ch-'0',ch=getchar();
return fu?-shu:shu;
}
void updat(int o){
tre[o].tot=tre[lsn(o)].tot^tre[rsn(o)].tot^tre[o].val;
}
bool Dwhi(int o){return rsn(tre[o].fa)==o;}
bool isrot(int o){return lsn(tre[o].fa)^o&&rsn(tre[o].fa)^o;}
void pudown(int o){
if(!tre[o].laz)return;
tre[lsn(o)].laz^=1,tre[rsn(o)].laz^=1;
swap( lsn(o),rsn(o) );
tre[o].laz=0;
}
void puall(int o){
if( !isrot(o) )puall(tre[o].fa);
pudown(o);
}
void rota(int o){
int y=tre[o].fa,z=tre[y].fa,whi=Dwhi(o);
int fawhi=( isrot(y)?-1:Dwhi(y) ),v=tre[o].son[whi^1];
tre[v].fa=y,tre[y].son[whi]=v;
tre[y].fa=o,tre[o].son[whi^1]=y;
tre[o].fa=z;if(~fawhi)tre[z].son[fawhi]=o;
updat(y),updat(o);
}
void Splay(int o){
puall(o);
while( !isrot(o) ){
int y=tre[o].fa;
if( !isrot(y) ){
Dwhi(o)==Dwhi(y)?rota(y):rota(o);
}
rota(o);
}
}
void aces(int o){
int las=0;
while(o){
Splay(o),rsn(o)=las,updat(o);
las=o,o=tre[o].fa;
}
}
void Mroot(int o){aces(o),Splay(o),tre[o].laz^=1;}
void split(int o1,int o2){Mroot(o1),aces(o2),Splay(o2);}
int Froot(int o){
aces(o),Splay(o);
while( lsn(o) )pudown(o),o=lsn(o);
Splay(o);
return o;
}
void link(int o1,int o2){
Mroot(o1);
if(Froot(o2)==o1)return;
tre[o1].fa=o2;
}
void cut(int o1,int o2){
if( Froot(o1)^Froot(o2) )return;
split(o1,o2);
if(tre[o1].fa^o2|| lsn(o1) || rsn(o1) )return;
tre[o1].fa=lsn(o2)=0;
updat(o2);
}
int main(){
n=rd(),T=rd();
rep(i,1,n)tre[i].val=rd(),updat(i);
while(T--){
int sys=rd(),p=rd(),q=rd();
if(sys==0)split(p,q),printf("%d\n",tre[q].tot);
if(sys==1)link(p,q);
if(sys==2)cut(p,q);
if(sys==3)Mroot(p),tre[p].val=q,updat(p);
}
return 0;
}