定义

动态树问题 ,是一类要求维护一个有根树森林,支持对树的分割, 合并等操作的问题。

\(RobertE.Tarjan\) 为首的科学家们提出解决算法 \(Link-Cut Trees\),简称 \(lct\)

在处理树上的很多询问问题的时候,我们常常会用到轻重链剖分,但是它只能维护当树的形态保持不变的时候的信息。

那么在树形态发生变化的时候,轻重链剖分就失去了效果,这个时候我们就要用到 \(lct\)

个人感觉这篇博客讲的不错

前置知识:spaly

\(splay\)\(lct\) 的辅助树

如果不会 \(splay\) 可以去我的博客学习一下

性质

\(1\)、和轻重链剖分相同的是,\(lct\) 也对要维护的树进行了划分,将链分为了实链和虚链。

每一个 \(Splay\) 维护的是一条从上到下按在原树中深度严格递增的路径,且中序遍历 \(Splay\) 得到的每个点的深度序列严格递增。

\(2\)、每个节点包含且仅包含于一个 \(Splay\)

\(3\)、边分为实边和虚边,实边包含在 \(Splay\) 中,而虚边总是由一棵 \(Splay\) 指向另一个节点(指向该 \(Splay\) 中中序遍历最靠前的点在原树中的父亲)。

如果一个节点有多个儿子,那么该节点只会认用实边相连的那个儿子,对于虚边相连的儿子,则会认父不认子。

操作

1、结构体定义

个人的写法是把 \(lct\) 封装到了结构体里

int ch[maxn][2],fa[maxn],val[maxn],sum[maxn],top,sta[maxn],rev[maxn];
//ch[0/1]:左/右儿子  fa:父亲节点 val:该节点的权值 
//sum:该节点及其子树的权值之和 sta&top:push_down时用 rev:翻转标记

2、标记上传和下放

类似于线段树

要注意的是,线段树的父亲节点并不是一个真实的节点,而 \(spaly\) 的父亲节点代表了原树中真实存在的一个节点

void push_up(rg int da){
	sum[da]=sum[ch[da][0]]^sum[ch[da][1]]^val[da];
}
void push_down(rg int da){
	rg int lc=ch[da][0],rc=ch[da][1];
	if(rev[da]){
		rev[lc]^=1,rev[rc]^=1,rev[da]^=1;
		std::swap(ch[da][0],ch[da][1]);
	}
}

3、isroot操作

判断某个节点是否是该节点所在的 \(splay\) 的根节点

利用了性质 \(3\)

bool isroot(rg int da){
	return (ch[fa[da]][0]!=da)&&(ch[fa[da]][1]!=da);
}

4、splay操作

\(splay\) 板子的区别就是多了 \(push\_down\) 操作

void xuanzh(rg int x){
	rg int y=fa[x];
	rg int z=fa[y];
	rg int k=(ch[y][1]==x);
	if(!isroot(y)){
		ch[z][ch[z][1]==y]=x;
	}
	fa[x]=z;
	ch[y][k]=ch[x][k^1];
	fa[ch[x][k^1]]=y;
	ch[x][k^1]=y;
	fa[y]=x;
	push_up(y);
	push_up(x);
}
void splay(rg int x){
	sta[top=1]=x;
	for(rg int i=x;!isroot(i);i=fa[i]) sta[++top]=fa[i];
	for(rg int i=top;i>=1;i--) push_down(sta[i]);
    //把需要下传标记的提前存起来一起修改
	while(!isroot(x)){
		rg int y=fa[x];
		rg int z=fa[y];
		if(!isroot(y)){
			(ch[z][1]==y)^(ch[y][1]==x)?xuanzh(x):xuanzh(y);
		}
		xuanzh(x);
	}
}

5、access操作

比较重要的一个操作

目的是打通根节点到指定节点的实链,使得一条中序遍历以根开始、以指定点结束的 \(Splay\) 出现

void access(rg int x){
	for(rg int y=0;x;y=x,x=fa[x]){
		splay(x);
		ch[x][1]=y;
		push_up(x);
	}
}

6、makeroot操作

使某一个节点成为整个联通块的根

\(access\) 之后已经打通了一条当前点到根节点的路径

此时 \(x\)\(Splay\) 中一定是深度最大的点

\(x\ splay\) 一下,\(x\) 将没有右子树(性质\(1\)

于是翻转整个 \(Splay\),使得所有点的深度都倒过来了

\(x\) 没了左子树,成了深度最小的点(根节点),达到了我们的目的

void makeroot(rg int x){
	access(x);
	splay(x);
	rev[x]^=1;
	push_down(x);
}

7、findroot操作

找出指定点所在的联通块的根节点

和上一个操作一样,只不过这一次我们不再进行翻转操作,而是一直跳左子树

最后跳到的节点就是根

int findroot(rg int x){
	access(x);
	splay(x);
	while(ch[x][0]){
		push_down(x);
		x=ch[x][0];
	}
	splay(x);
	return x;
}

8、split操作

提取 \(x\)\(y\) 的路径

先让 \(x\) 成为根节点,再打通 \(y\) 到根节点的路径

void split(rg int x,rg int y){
	makeroot(x);
	access(y);
	splay(y);
}

9、删边、连边操作

先让 \(x\) 成为根节点,再判断联通性

void link(rg int x,rg int y){
	makeroot(x);
	if(findroot(y)!=x) fa[x]=y;
}
void cut(rg int x,rg int y){
	makeroot(x);
	if(findroot(y)==x && fa[y]==x && ch[y][0]==0){
		fa[y]=ch[x][1]=0;
		push_up(x);
	}
}

10、求LCA操作

两遍 \(access\)

int access(rg int x){
   for(rg int y=0;x;y=x,x=fa[x]){
   	splay(x);
   	ch[x][1]=y;
   	push_up(x);
   }
   return y;// 多了一个返回值
}

然后求 \(LCA\) 的过程就出来了。

int LCA(int x,int y){ 
   if(findroot(x)!=findroot(y))// 必须的特判。
   	return -1;
   access(x);
   return access(y); 
} 

完整模板

洛谷P3690

#include<cstdio>
#include<iostream>
#define rg register
inline int read(){
	rg int x=0,fh=1;
	rg char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
const int maxn=1e6+5;
int n,m;
struct LCT{
	int ch[maxn][2],fa[maxn],val[maxn],sum[maxn],top,sta[maxn],rev[maxn];
	void push_up(rg int da){
		sum[da]=sum[ch[da][0]]^sum[ch[da][1]]^val[da];
	}
	void push_down(rg int da){
		rg int lc=ch[da][0],rc=ch[da][1];
		if(rev[da]){
			rev[lc]^=1,rev[rc]^=1,rev[da]^=1;
			std::swap(ch[da][0],ch[da][1]);
		}
	}
	bool isroot(rg int da){
		return (ch[fa[da]][0]!=da)&&(ch[fa[da]][1]!=da);
	}
	void xuanzh(rg int x){
		rg int y=fa[x];
		rg int z=fa[y];
		rg int k=(ch[y][1]==x);
		if(!isroot(y)){
			ch[z][ch[z][1]==y]=x;
		}
		fa[x]=z;
		ch[y][k]=ch[x][k^1];
		fa[ch[x][k^1]]=y;
		ch[x][k^1]=y;
		fa[y]=x;
		push_up(y);
		push_up(x);
	}
	void splay(rg int x){
		sta[top=1]=x;
		for(rg int i=x;!isroot(i);i=fa[i]) sta[++top]=fa[i];
		for(rg int i=top;i>=1;i--) push_down(sta[i]);
		while(!isroot(x)){
			rg int y=fa[x];
			rg int z=fa[y];
			if(!isroot(y)){
				(ch[z][1]==y)^(ch[y][1]==x)?xuanzh(x):xuanzh(y);
			}
			xuanzh(x);
		}
	}
	void access(rg int x){
		for(rg int y=0;x;y=x,x=fa[x]){
			splay(x);
			ch[x][1]=y;
			push_up(x);
		}
	}
	void makeroot(rg int x){
		access(x);
		splay(x);
		rev[x]^=1;
		push_down(x);
	}
	int findroot(rg int x){
		access(x);
		splay(x);
		while(ch[x][0]){
			push_down(x);
			x=ch[x][0];
		}
		splay(x);
		return x;
	}
	void split(rg int x,rg int y){
		makeroot(x);
		access(y);
		splay(y);
	}
	void link(rg int x,rg int y){
		makeroot(x);
		if(findroot(y)!=x) fa[x]=y;
	}
	void cut(rg int x,rg int y){
		makeroot(x);
		if(findroot(y)==x && fa[y]==x && ch[y][0]==0){
			fa[y]=ch[x][1]=0;
			push_up(x);
		}
	}
}lct;
int main(){
	n=read(),m=read();
	for(rg int i=1;i<=n;i++) lct.sum[i]=lct.val[i]=read();
	rg int aa,bb,cc;
	for(rg int i=1;i<=m;i++){
		aa=read(),bb=read(),cc=read();
		if(aa==0){
			lct.split(bb,cc);
			printf("%d\n",lct.sum[cc]);
		} else if(aa==1){
			lct.link(bb,cc);
		} else if(aa==2){
			lct.cut(bb,cc);
		} else {
			lct.splay(bb);
			lct.val[bb]=cc;
		}
	}
	return 0;
}

复杂度证明

传送门

一些题目

类型一、维护链信息

[国家集训队]Tree II

注意区间标记先乘后除

然后就和普通的线段树一样标记下放即可

如果没有删边和加边操作,也可以用树链剖分实现
,但是复杂度要多一个 \(log\)

有的时候要维护的信息不是在点上而是在边上

此时我们就需要把边看成点

例如有一条边 \((u,v)\),编号为 \(id\)

那么我们需要在 \(lct\) 中连两条边 \((u,id+n)(v,id+n)\)

边的信息储存在 \(id+n\) 这个点上

类型二、动态维护联通性

[SDOI2008]洞穴勘测

这道题用可撤销并查集按秩合并也可以做

换成 \(lct\) 也是一个板子

只要判断两个点 \(findroot\) 得到的值是否一样即可

类型三、最小生成树一类的问题

最小差值生成树

[NOI2014]魔法森林

[Cnoi2019]须臾幻境

[WC2006]水管局长

这种题大部分都需要在边权上维护一个最大/最小值,然后按照边权从大到小/从小到大排序

遍历到一条边时,如果这条边所连的两个点已经在同一个同一个联通块中,根据需要删边/加边

建议多加练习

版权声明:本文为liuchanglc原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/liuchanglc/p/14165122.html