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);}

解释一下为什么是Son[x][1]=tSon[x][1]=t,因为我们要将新的链连上去,势必要删除原先深度大的链,否则就不是一条链了(建议再次看一下开头的重点)

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,所以我们更新Fa[x]Fa[x]Son[y][0]Son[y][0],因为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的根,然后修改Fa[x]=yFa[x]=y(因为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;
}
posted @ 2019-03-12 13:24  XSamsara  阅读(495)  评论(0编辑  收藏  举报