LCT

就是将各种数列的维护改到树上,如查询链上和,子树和,修改节点数值。

处理的基本思路是将树上问题重新转化到链上,而转化往往是基于给树上的节点按照某种顺序标号(DFS序or树链剖分),然后每次处理标号连续的一段或若干段。

DFS序

咕咕咕

树链剖分

咕咕咕

(以后复习到再写/cy)

LCT

\(link-cut-tree\)\(Tarjan\) 等人发明,可以处理更一般的动态树问题的,由势能分析得到均摊复杂度为\(O(n log n)\)

动态维护的剖分

\(link-cut-tree\) 维护的是一片森林,能支持的操作包括:查询/修改某条链上的信息、砍掉一条边、在两个点之间连边(需要保证连接之后不可以出环)、换根

\(lct\) 的实质是动态维护树的实链剖分/重链剖分——即将一棵树划分成若干条不相交的重链,每条重链使用一棵 \(Splay\)维护,没有重边的单点也视作一条重链。

每次通过将轻边打成重边的方式来打通某条特定的链。假如我们想得到点 \(u\) 到点 \(v\) 的链,方法是把它们打成重链,然后在重链中舍去两端的东西。

最终的目的就是让 \(u\)\(v\) 同处在一棵 \(Splay\) 中,且这棵 \(Splay\) 只包含 \(u\)\(v\) 这条路径上的点,这样我们就可以进行操作了。

实现

用这种方式表现轻边的连接方式:让每棵 Splay 的根的父亲指向这段重边的轻边父亲,而它却不是轻边父亲的左右儿子中的任何一个。

对于同一棵重链,我们约定靠上的点在左边,靠下的在右边。那么我们定义打通操作是将某个 Splay 的根和它的轻父亲打成同一棵 Splay,那么它父亲原来的右儿子就会退化成为轻儿子。我们成这个操作为 splice。

很轻松地我们可以将一个点一直 splice 到根,我们把通到根的操作称为 expose。将一个点 expose 到根之后,将它的右子树断掉,再将剩下的 Splay(只包含它到根的那条链)reverse 一下,就可以将这个节点变为根。

这样我们很简单地做完了换根 (reroot) 操作。提取链的操作更加容易:reroot(u); reroot(v); 那么此时的点 v 为根的 Splay 就为所求了。

还有两个操作 link 和 cut 没有处理,其实都是将两个节点 reroot 之后直接断/连接边就可以了。

性质

把在某个\(splay\)内部的边划分为实边,两个\(splay\)之间的划分为虚边

每个\(splay\)维护一条原树中深度严格递增的路径,中序遍历\(splay\),得到的点序列深度严格递增。

每个点只属于一个\(splay\)

一个父亲可能有多个儿子,在儿子中只有一个实儿子,让父亲指向的边变成虚边,由对应儿子所属的\(splay\)的根节点的父亲节点指向该点,这样的话父亲不能直接访问虚儿子。

虚边总是由一颗\(splay\)指向另一个\(splay\)中中序遍历最靠前的点的父亲。

(结合图感性理解一下

左边的树是重链剖分后的形态。

左树中的\(C\) -> \(A\),由于\(C\)\(splay\)中的根节点为\(G\),所以\(G\) -> \(A\)

即:在\(splay\)中找父节点,再用父节点与在原树中相连的点连接。

操作

access(n)

结果:把从根节点到\(n\)的路径变成实链,使其在 splay 的中序遍历中,以根节点开始,以指定点结束。

过程:

  1. \(n\)转到自己\(splay\)的根的位置。
  2. 断掉\(n\)的右儿子,实边 ->虚边。(因为右儿子深度更大,中序遍历会在\(n\)的后面,就不是以\(n\)结尾了。
  3. \(n\)所属的\(splay\)的虚边指向节点\(v\),转到属于它(虚边指向节点\(v\))的\(splay\)的根。
  4. \(v\)的右儿子断掉,把\(n\)连接成为\(v\)的右儿子(此时,\(n\)中仅含有左儿子,由于所在\(splay\)指向了\(v\),所以\(v\)的深度小于\(splay\)中的点。
  5. 重复执行。假设当前\(splay\)的根节点为\(x\) , \(fa(x)=y\) ,那么先\(splay(y)\)把y转到\(y\)所在\(splay\)的根节点,再断开\(y\)的右儿子,换成\(x\)

trick:认父不认子。

code:

il void access(int x){
	for(int y=0;x;x=f[y=x]){//把自己所在splay的父亲转成自己 
		splay(x);//x->now father y->x 
		s[x][1]=y;//把右儿子变成last x
		pushup(x);//儿子变了要上传信息 
	}
}

makeroot(x)

\(x\)变成根。

很多时候,我们要处理的路径并不是从根到另一个节点的,而是没有祖孙关系的两个节点。

所以通过\(makeroot(x)\),让\(x\)变为原树的根。

过程:

  1. access(x),让\(x\)和根在同一个\(splay\)中,同时让中序遍历最后一个节点是\(x\)(去掉右子树)

  2. 直接翻转。(\(splay\)可以很好维护区间翻转,具体方法是打标记。可参考文艺平衡树

    证明:

    \(x\)原本没有右子树,翻转后\(x\)就没有了左子树,没有左子树的点,一定是\(splay\)中序遍历最小的点,既然我们保证\(splay\)中序遍历所经过的点深度严格单调递增,那么我们就让它成为了这个\(splay\)中深度最小的点,而这个\(splay\)是包含根节点的,根节点本来是深度最小的,这样\(x\)就变成了根。

    说人话就是:本来在这个\(splay\)中,\(x\)是最后一个,翻转后就成了根,即第一个。

code:

il void pushr(int x){//翻转左右儿子 
	swap(s[x][0],s[x][1]);
	tag[x]^=1;//翻转标记 
}

il void pushdown(int x){//下传懒标记 
	if(tag[x]){
		if(s[x][0]) pushr(s[x][0]);
		if(s[x][1]) pushr(s[x][1]);
		tag[x]=0; 
	}
}

il void makeroot(int x){
	access(x);//打通x 
	splay(x);//把x转到根 
	pushr(x);//翻转后打上标记 
}

findroot(x)

主要用于判断连通性,找到\(x\)所在原树的根,如果 findroot(x)==findroot(y)/mekeroot(x)if(findroot(y)==x)说明两个点在同一个树里。

il int findroot(int x){//常数大 尽量减少使用 
	access(x);splay(x);//把x转到根 
	while(s[x][0]){//找左儿子->不断靠近根,深度逐渐减小 
		pushdown(x);
		x=s[x][0];
	}//找到根节点之后停止,把根节点再转回去 
	splay(x);
	return x;
}

split(x,y)

就是拿出一棵 \(Splay\),维护的是 \(x\)\(y\) 的路径。

il void spilt(int x,int y){
	makeroot(x);//让x->根
	access(y);//打通y->x的路
	splay(y);//把y转到根,视情况而决定加不加 
}

link(x,y)

连接\(x\), \(y\),先将\(x\)转到根,再把\(x\)的父亲设为\(y\),建立了一条虚边。

il void link(int x,int y){
	makeroot(x);//把x转到根
	if(findroot(y)!=x) f[x]=y;//判断是否合法,连接虚边不能在同一棵splay中 
}

cut(x,y)

\(Cut\) 有两种情况,保证合法和不一定保证合法。

如果保证合法,直接 Split(x, y),这时候 y 是根,x 一定是它的儿子,双向断开即可。就像这样:

inline void Cut(int x, int p) {
  makeRoot(x), Access(p), Splay(p), ls = f[x] = 0;
}

如果是不保证合法,我们需要判断一下是否有,可以选择使用 \(map\) 存一下,但是这里有一个利用性质的方法:

想要删边,必须要满足如下三个条件:

  1. \(x\) ,\(y\) 连通。
  2. \(x\) , \(y\) 的路径上没有其他的链。
  3. \(x\) 没有右儿子。

总之就是\(x\) ,\(y\) 之间有边。

il void cut(int x,int y){
	makeroot(x);//让x做根 
	if(findroot(y)==x&&f[y]==x&&!s[y][0]){//如果在同一个splay中并且x,y的路径上没有其他的链并且x没有右儿子
		f[y]=s[x][1]=0;
		pushup(x);
	}
}

IsRoot

判断是否为根。

il bool IsRoot(int x){//s[f[]][0/1]父亲的儿子信息里面只有实儿子 
	return s[f[x]][0]==x||s[f[x]][1]==x;
}

splay相关

pushdown

要从上往下传递标记,所以要开一个栈来储存从当前点到根的整条路径,然后先\(pushdown\),再用普通\(splay\)的方式上传。

il void splay(int x){
	int y=x,z=0;
	st[++z]=y;//栈来储存经过的点 
	while(IsRoot(y)) st[++z]=y=f[y];//将标记沿着路径下传 
	while(z) {pushdown(st[z--]);}
	while(IsRoot(x)){//正常splay 
		y=f[x];z=f[y];
		if(IsRoot(y))
			rotate((s[y][0]==x)^(s[z][0]==y)?x:y);
		rotate(x);
	}
	pushup(x);
}

其他具体情况具体分析。


例题

动态树

const int N=1e5+10;
const int M=3e5+10;
int n,m,op,x,y,w[N];

namespace LCT{
	#define ls(k) s[k][0]
	#define rs(k) s[k][1]
	int s[N][2];
	int f[N],sum[N];//路径权值和 
	int tag[N];//点权,翻转标记 
	int laz[N],siz[N];//辅助树上子树大小
	int st[N];//开栈 
	
	il void pushup(int k){siz[k]=siz[ls(k)]^siz[rs(k)]^w[k];}
	
	il void pushr(int x){//翻转左右儿子 
	    swap(s[x][0],s[x][1]);
	    tag[x]^=1;//翻转标记 
	}
	
	il void pushdown(int x){//下传懒标记 
		if(tag[x]){
			if(s[x][0]) pushr(s[x][0]);
			if(s[x][1]) pushr(s[x][1]);
			tag[x]=0; 
		}
	}
	
	il bool IsRoot(int x){//s[f[]][0/1]父亲的儿子信息里面只有实儿子 
	    return s[f[x]][0]==x||s[f[x]][1]==x;
	}
	
	il void rotate(int x){//旋转
	    int y=f[x],z=f[y];//y是x的父亲,z是x的爷爷
	    int k=s[y][1]==x;//k代表x是y的左儿子还是右儿子
	    if(IsRoot(y)) s[z][s[z][1]==y]=x;
	    s[y][k]=s[x][k^1],f[s[x][k^1]] = y;
		s[x][k^1]=y,f[y]=x,f[x]=z;
	    pushup(x);pushup(y);
	}
	
	il void splay(int x){
	    int y=x,z=0;
	    st[++z]=y;//栈来储存经过的点 
	    while(IsRoot(y)) st[++z]=y=f[y];//将标记沿着路径下传 
	    while(z) {pushdown(st[z--]);}
	    while(IsRoot(x)){//正常splay 
	        y=f[x];z=f[y];
	        if(IsRoot(y))
	            rotate((s[y][0]==x)^(s[z][0]==y)?x:y);
	        rotate(x);
	    }
	    pushup(x);
	}
	
	il void access(int x){
	    for(int y=0;x;x=f[y=x]){//把自己所在splay的父亲转成自己 
	        splay(x);//x->now father y->x 
	        rs(x)=y;//把右儿子变成last x
	        pushup(x);//儿子变了要上传信息 
	    }
	}
/*  
	il void access(int x){
	    for(int y=0;x;x=f[y=x]){//把自己所在splay的父亲转成自己 
	        splay(x);//x->now father y->x 
	        s[x][1]=y;//把右儿子变成last x
	        pushup(x);//儿子变了要上传信息 
	    }
	}
*/	
	il void makeroot(int x){
	    access(x);//打通x 
	    splay(x);//把x转到根 
	    pushr(x);//翻转后打上标记 
	}
	
	il int findroot(int x){//常数大 尽量减少使用 
	    access(x);splay(x);//把x转到根 
	    while(ls(x)){//找左儿子->不断靠近根,深度逐渐减小 
	        pushdown(x);
	        x=ls(x);
	    }//找到根节点之后停止,把根节点再转回去 
	    splay(x);
	    return x;
	}
	
	il void split(int x,int y){
	    makeroot(x);//让x->根
	    access(y);//打通y->x的路
	    splay(y);//把y转到根,视情况而决定加不加 
	}
	
	il void cut(int x,int y){
	    makeroot(x);//让x做根 
	    if(findroot(y)==x&&f[y]==x&&!s[y][0]){//如果在同一个splay中并且x,y的路径上没有其他的链并且x没有右儿子
	        f[y]=s[x][1]=0;
	        pushup(x);
	    }
	}
	
	il void link(int x,int y){
	    makeroot(x);//把x转到根
	    if(findroot(y)!=x) f[x]=y;//判断是否合法,连接虚边不能在同一棵splay中 
	}
}

signed main(){
	n=read();m=read();
	rep(i,1,n)
		w[i]=read();
	rep(i,1,m){
		op=read();x=read();y=read();
		if(op==0){
			LCT::split(x,y);
			cout<<LCT::siz[y]<<'\n';
		}
		else if(op==1){
			LCT::link(x,y);
		}
		else if(op==2){
			LCT::cut(x,y);
		}
		else if(op==3){
			LCT::splay(x);w[x]=y;//把x先转到根上去 
		}
	}
	return 0;
}

洞穴勘测

const int N=1e5+10;
const int M=3e5+10;
int n,m,x,y;
string op;

namespace LCT{
	#define ls(k) s[k][0]
	#define rs(k) s[k][1]
	int s[N][2];
	int f[N],sum[N];//路径权值和 
	int tag[N];//点权,翻转标记 
	int laz[N],siz[N];//辅助树上子树大小
	int st[N];//开栈 
	
	il void pushup(int k){siz[k]=siz[ls(k)]+siz[rs(k)]+1;}//Wrong
	
	il void pushr(int x){//翻转左右儿子 
	    swap(s[x][0],s[x][1]);
	    tag[x]^=1;//翻转标记 
	}
	
	il void pushdown(int x){//下传懒标记 
		if(tag[x]){
			if(s[x][0]) pushr(s[x][0]);
			if(s[x][1]) pushr(s[x][1]);
			tag[x]=0; 
		}
	}
	
	il bool IsRoot(int x){//s[f[]][0/1]父亲的儿子信息里面只有实儿子 
	    return s[f[x]][0]==x||s[f[x]][1]==x;
	}
	
	il void rotate(int x){//旋转
	    int y=f[x],z=f[y];//y是x的父亲,z是x的爷爷
	    int k=s[y][1]==x;//k代表x是y的左儿子还是右儿子
	    if(IsRoot(y)) s[z][s[z][1]==y]=x;//Wrong
	    s[y][k]=s[x][k^1],f[s[x][k^1]] = y;
		s[x][k^1]=y,f[y]=x,f[x]=z;
	    pushup(x);pushup(y);
	}
	
	il void splay(int x){
	    int y=x,z=0;
	    st[++z]=y;//栈来储存经过的点 
	    while(IsRoot(y)) st[++z]=y=f[y];//将标记沿着路径下传 
	    while(z) {pushdown(st[z--]);}
	    while(IsRoot(x)){//正常splay 
	        y=f[x];z=f[y];
	        if(IsRoot(y))
	            rotate((s[y][0]==x)^(s[z][0]==y)?x:y);
	        rotate(x);
	    }
	    pushup(x);
	}
	
	il void access(int x){
	    for(int y=0;x;x=f[y=x]){//把自己所在splay的父亲转成自己 
	        splay(x);//x->now father y->x 
	        rs(x)=y;//把右儿子变成last x
	        pushup(x);//儿子变了要上传信息 
	    }
	}
		
	il void makeroot(int x){
	    access(x);//打通x 
	    splay(x);//把x转到根 
	    pushr(x);//翻转后打上标记 
	}
	
	il int findroot(int x){//常数大 尽量减少使用 
	    access(x);splay(x);//把x转到根 
	    while(ls(x)){//找左儿子->不断靠近根,深度逐渐减小 
	        pushdown(x);
	        x=ls(x);
	    }//找到根节点之后停止,把根节点再转回去 
	    splay(x);
	    return x;
	}
	
	il void split(int x,int y){
	    makeroot(x);//让x->根
	    access(y);//打通y->x的路
	    splay(y);//把y转到根,视情况而决定加不加 
	}
	
	il void cut(int x,int y){
	    makeroot(x);//让x做根 
	    if(findroot(y)==x&&f[y]==x&&!s[y][0]){//如果在同一个splay中并且x,y的路径上没有其他的链并且x没有右儿子
	        f[y]=s[x][1]=0;
	        pushup(x);
	    }
	}
	
	il void link(int x,int y){
	    makeroot(x);//把x转到根
	    if(findroot(y)!=x) f[x]=y;//判断是否合法,连接虚边不能在同一棵splay中 
	}
}

signed main(){
	n=read();m=read();
	rep(i,1,m){
		cin>>op;x=read();y=read();
		if(op[0]=='Q'){
			LCT::makeroot(x);
			if(LCT::findroot(y)==x) cout<<"Yes"<<'\n';
			else cout<<"No"<<'\n';
		}
		else if(op[0]=='C'){
			LCT::link(x,y);
		}
		else if(op[0]=='D'){
			LCT::cut(x,y);
		}
	}
	return 0;
}
posted @ 2023-06-19 19:37  XYini  阅读(8)  评论(0)    收藏  举报