LCT动态树【史上最精简易懂的LCT讲解】
Link Cut Tree(动态树,LCT)
介绍
首先简单介绍一下Link Cut Tree,将一棵树分成轻边和重链,类似于树链剖分,但是树剖是静态的。LCT可以用于动态的加点和删点,甚至还可以换根。也就是LCT维护了一个动态的树。
LCT中的每棵Splay都维护了一条重链(实际上是实链,这里统一讲重链)上的答案。多颗Splay连成一棵树,这棵Splay满足左儿子在树上的深度(这里深度指的是在数上的深度(下同),不是Splay上的深度)小于根,右儿子大于根。所以这条Splay每个节点的深度一定是不同的,所以一棵Splay树存的节点是从上往下的一条路径。Splay的形状不是在树上节点的形状。(这段话都是重点,一定要牢记,否则后面很多代码理解不了)
Splay直接的连接方式是连父不连子的,也就是Fa[x]=y,Son[y]!=x。
实现方法
首先介绍一下变量:
int Top,que[MAXN];//用于Splay
int Son[MAXN][2];//(0/1)左/右儿子节点
int Fa[MAXN];//父节点
int W[MAXN];//权值
int Rtd[MAXN];//Lazy标记
接下来是Splay的基本操作:
int Get(int x){return x==Son[Fa[x]][1];}//判断是左儿子还是右儿子
void PushUp(int x){W[x]=W[Son[x][1]]^W[Son[x][0]]^a[x];}//更新当前节点的值
void PushDown(int x){//懒惰标记
int &L=Son[x][0],&R=Son[x][1];
if(Rtd[x]){
Rtd[L]^=1;Rtd[R]^=1;Rtd[x]^=1;
swap(L,R);
}
}
bool IsRoot(int x){return Son[Fa[x]][0]!=x&&Son[Fa[x]][1]!=x;}
//判断是否是Splay的根节点,因为Splay森林是连父不连子的,所以需要这么判。
void Rotate(int x){//旋转,将x变成y的父节点
int y=Fa[x],z=Fa[y],L,R;
R=(L=Get(x))^1;
if(!IsRoot(y)) Son[z][Son[z][1]==y]=x;//如果y不是根节点,那么将z的儿子y变成x
Fa[x]=z;Fa[y]=x;Fa[Son[x][R]]=y;
Son[y][L]=Son[x][R];Son[x][R]=y;
PushUp(y);PushUp(x);
}
void Splay(int x){//旋到Splay的根
que[Top=1]=x;
for(int i=x;!IsRoot(i);i=Fa[i]) que[++Top]=Fa[i];
for(int i=Top;i;i--) PushDown(que[i]);//下传标记,一定要从上往下
while(!IsRoot(x)){//将x旋到根
int y=Fa[x],z=Fa[y];
if(!IsRoot(y)) (Son[y][0]==x)^(Son[z][0]==y)?Rotate(x):Rotate(y);
Rotate(x);
}
}
然后重点来了:
Access(x):删除x到根路径上所有连向其他点的重链,重新拉一条x到根的重链。
实现也很简单,Splay到根,更新当前节点的儿子就可以了。
如果想知道Splay是如何实现这个,可以看https://www.cnblogs.com/flashhu/p/8324551.html,里面用图详细的用图像模拟了Access(x)的过程。
void Access(int x){for(int t=0;x;t=x,x=Fa[x])Splay(x),Son[x][1]=t,PushUp(x);}
解释一下为什么是,因为我们要将新的链连上去,势必要删除原先深度大的链,否则就不是一条链了(建议再次看一下开头的重点)
MakeRoot(x):将x变成整棵树的根(不是Splay的根)。
所以要把x变成根就只需要让所有Splay的父亲最终指向x所在Splay。
所以先Access(x),Splay(x),把现在的根和将成为根的x链在一棵Splay中。
但是我们注意到,由于x成为了新的根,所以它和原来的根所在的Splay中深度作为关键字的性质遭到了破坏:新根x应该是Splay中深度最小的,但是之前的操作并不会改变x的深度(也就是目前x依旧是当前Splay中深度最深的)。
所以,我们需要把所在的这棵Splay翻转过来,x就变成了这整棵树深度最小的点(再次建议看一下重点)。
void MakeRoot(int x){Access(x);Splay(x);Rtd[x]^=1;}
Fnd_Root(x):找到x所在树的根节点(主要来判断连通性,可以看做并查集)。
我们知道深度最小的是根,那么一直不停的找左儿子就可以了
int Fnd_Root(int x){Access(x);Splay(x);while(Son[x][0]) x=Son[x][0];return x;}
Split(x,y):拉出一条x—y的路径成为一棵Splay。
先将MakeRoot(x),将x变成根,然后从y连接一条重链到根(也就是x),这样x和y就再同一棵Splay中了,然后将Splay(y)更新答案就可以了。
void Split(int x,int y){MakeRoot(x);Access(y);Splay(y);}
Cut(x,y):删除一条x到y的边
当然要分离出x,y然后进行操作,所以Split(x,y),然后当前的根是y,所以我们更新和,因为x一定是y的左儿子,否则表示x和y之间没有一条边,但是可以有路径。
void Cut(int x,int y){
Split(x,y);
if(Son[x][1]||Fa[x]!=y||Son[y][Get(x)^1]) return;
Son[y][0]=Fa[x]=0;
}
Link(x,y):连接x和y
这个最好理解了,将x变成LCT的根,然后修改(因为Splay之间的连接是连父不连子的)。
void Lnk(int x,int y){MakeRoot(x);Fa[x]=y;}
比如说要得到x到y路径上的答案时,首先Split(x,y),将路径分离出来,当前Splay就是这条路径,根是y,所以直接输出y的值就可以了。
Split(x,y);
printf("%d\n",W[y]);
完整代码
#include<cstdio>
#include<cctype>
#include<algorithm>
#define MAXN 300005
using namespace std;
int n,m,a[MAXN];
struct Link_Cut_Tree{
int Top,Son[MAXN][2],Fa[MAXN],W[MAXN],que[MAXN],Rtd[MAXN];
int Get(int x){return x==Son[Fa[x]][1];}//判断是否是右儿子
void PushUp(int x){W[x]=W[Son[x][1]]^W[Son[x][0]]^a[x];}//更新当前节点的值
void PushDown(int x){//懒惰标记
int &L=Son[x][0],&R=Son[x][1];
if(Rtd[x]){
Rtd[L]^=1;Rtd[R]^=1;Rtd[x]^=1;
swap(L,R);
}
}
bool IsRoot(int x){return Son[Fa[x]][0]!=x&&Son[Fa[x]][1]!=x;}//判断是否是根节点
void Rotate(int x){//旋转,将x变成y的父节点
int y=Fa[x],z=Fa[y],L,R;
R=(L=Get(x))^1;
if(!IsRoot(y)) Son[z][Son[z][1]==y]=x;//如果y不是根节点,那么将z的儿子y变成x
Fa[x]=z;Fa[y]=x;Fa[Son[x][R]]=y;
Son[y][L]=Son[x][R];Son[x][R]=y;
PushUp(y);PushUp(x);
}
void Splay(int x){
que[Top=1]=x;
for(int i=x;!IsRoot(i);i=Fa[i]) que[++Top]=Fa[i];//一定要从上往下
for(int i=Top;i;i--) PushDown(que[i]);
while(!IsRoot(x)){//将x旋到根
int y=Fa[x],z=Fa[y];
if(!IsRoot(y)) (Son[y][0]==x)^(Son[z][0]==y)?Rotate(x):Rotate(y);
Rotate(x);
}
}
void Access(int x){for(int t=0;x;t=x,x=Fa[x]) Splay(x),Son[x][1]=t,PushUp(x);}//连接一条重链到根
void MakeRoot(int x){Access(x);Splay(x);Rtd[x]^=1;}//将x变成根
int Fnd(int x){Access(x);Splay(x);while(Son[x][0]) x=Son[x][0];return x;}//找x的根节点
void Split(int x,int y){MakeRoot(x);Access(y);Splay(y);}//拉出一条x到y的路径为一个Splay
void Cut(int x,int y){//删除一条x到y的边
Split(x,y);
if(Son[x][1]||Fa[x]!=y||Son[y][Get(x)^1]) return;
Son[y][0]=Fa[x]=0;
}
void Lnk(int x,int y){MakeRoot(x);Fa[x]=y;}//连一条x到y的轻边
}Tre;
int read(){
int ret=0;char ch=getchar();bool f=1;
for(;!isdigit(ch);ch=getchar()) f^=!(ch^'-');
for(; isdigit(ch);ch=getchar()) ret=(ret<<3)+(ret<<1)+ch-48;
return f?ret:-ret;
}
int main(){
#ifndef ONLINE_JUDGE
freopen("prob.in","r",stdin);
freopen("prob.out","w",stdout);
#endif
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),Tre.W[i]=a[i];
while(m--){
int opt=read(),x=read(),y=read();
if(opt==0){
Tre.Split(x,y);
printf("%d\n",Tre.W[y]);
}else
if(opt==1){
if(Tre.Fnd(x)!=Tre.Fnd(y))
Tre.Lnk(x,y);
}else
if(opt==2){
if(Tre.Fnd(x)==Tre.Fnd(y))
Tre.Cut(x,y);
}else
if(opt==3){
a[x]=y;Tre.Access(x);Tre.Splay(x);Tre.PushUp(x);
}
}
return 0;
}