LCT 小记

全程 Link-Cut Tree,是解决动态树问题的有力科技

——题记

简单实现

LCT 的形态直观上是一堆 Splay 的合体,每个 Splay 以时间戳为关键字,各个 Splay 通过虚边相连,可以唯一还原出一棵树

Splay 的划分依据为实链剖分,即在一条实链上的点放到一棵 Splay 里面,其他关联点通过虚边和其他 Splay 连接

一条虚边体现为儿子的父亲认做父亲,而父亲是不认这个儿子的

首先得先写 Splay 对吧,唯二不同的是 Splay 之前先跳到根把所有懒标记下放,以及旋转是不能贸然让父亲认儿子,需要判断是不是虚边

核心函数 —— \(access(x)\) 操作
经过这个操作后可以把 \(x\) 这个节点与整棵树的树根划分在一条实链里(即一棵 Splay 里),并且这条链在 \(x\) 处结束

至于具体怎么弄就不说了,反正代码异常简洁

void access(int x){
	for(int y=0;x;y=x,x=fa[x]){
		splay(x);
		son[x][1]=y;
		update(x);
	}
	return ;
}

\(makeroot(x)\) 操作 —— 将 \(x\) 弄到整棵树的树根

先 \(access\),再 \(splay\),这时虽然它到了平衡树的顶部,由于存在左儿子,它的时间戳没变
这是用一个神奇的操作 —— \(reverse\),就像文艺平衡树那样把它两个儿子颠倒一下,这个也需要通过打懒标记依次下放

\(link(x,y)\) 操作 —— 将两个点所在的树合并
把 \(x\) 转到它那棵树的根,直接认 \(y\) 当爹即可

\(cut(x,y)\) 操作 —— 将两个点间的连边删除
把 \(x\) 弄到根上,把 \(y\) 和它打通,再把 \(y\) 转到根上,由于它们在树上是相邻的,这时的状态一定是 Splay 里只有它们两个,直接不认父亲和不认儿子即可
(注:这里的“弄”指的是 \(makeroot\),这里的“转”指的是 \(splay\),习惯上代码里把前三步重新封装到一个 \(split\) 函数里)

这些就是核心函数了,对于询问,常形如“从 \(x\) 到 \(y\) 的路径上的 xxx 信息”
这时只需要做一遍 \(split\) 操作,\(y\) 节点上的信息就是链上的信息

这里放一下洛谷模板题的代码,背熟即可:

代码
#include<bits/stdc++.h>
using namespace std;
int n,m,op,x,y;
int read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=x*10+ch-48;
		ch=getchar();	
	}
	return x*f;
}
namespace LCT{
	const int maxn=2e5+5;
	int fa[maxn],son[maxn][2];
	int rev[maxn],val[maxn],sum[maxn];
	int sta[maxn],tp;
	
	void update(int x){
		sum[x]=sum[son[x][0]]^sum[son[x][1]]^val[x];
		return ;
	}
	void push_rev(int x){
		rev[x]^=1;
		swap(son[x][0],son[x][1]);
		return ;	
	}
	void down(int x){
		if(rev[x]){
			push_rev(son[x][0]);
			push_rev(son[x][1]);
			rev[x]=0;	
		}
		return ;
	}
	bool get(int x){
		return son[fa[x]][1]==x;	
	}
	bool isroot(int x){
		return son[fa[x]][0]!=x&&son[fa[x]][1]!=x;	
	}
	void rotate(int x){
		int f1=fa[x];
		int f2=fa[f1];
		bool id=get(x);
		son[f1][id]=son[x][id^1];
		fa[son[f1][id]]=f1;
		if(!isroot(f1))son[f2][get(f1)]=x;
		fa[x]=f2;
		son[x][id^1]=f1;
		fa[f1]=x;
		update(f1);
		return ;
	}
	void splay(int x){
		tp=0;
		int y=x;
		sta[++tp]=y;
		while(!isroot(y))sta[++tp]=y=fa[y];
		while(tp)down(sta[tp--]);
		while(!isroot(x)){
			int f1=fa[x];
			int f2=fa[f1];
			if(!isroot(f1)){
				rotate(get(x)==get(f1)?f1:x);
			}
			rotate(x);
		}
		update(x);
		return ;
	}
	void access(int x){
		for(int y=0;x;y=x,x=fa[x]){
			splay(x);
			son[x][1]=y;
			update(x);
		}
		return ;
	}
	void makeroot(int x){
		access(x);
		splay(x);
		push_rev(x);
		return ;	
	}
	void split(int x,int y){
		makeroot(x);
		access(y);
		splay(y);	
	}
	int findroot(int x){
		access(x);
		splay(x);
		while(son[x][0])x=son[x][0];
		splay(x);
		return x;	
	}
	void link(int x,int y){
		makeroot(x);
		if(findroot(y)!=x)fa[x]=y;
		return ;	
	}
	void cut(int x,int y){
		split(x,y);	
		if(son[y][0]==x){
			son[y][0]=fa[x]=0;
			update(y);	
		}
		return ;
	}
	int ask(int x,int y){
		split(x,y);
		return sum[y];
	}
	void modify(int x,int w){
		splay(x);
		val[x]=w;
		update(x);
		return ;	
	}
}
int main(){
	n=read();
	m=read();
	for(int i=1;i<=n;i++){
		LCT::val[i]=read();	
	}
	for(int i=1;i<=m;i++){
		op=read();
		x=read();
		y=read();
		switch (op){
			case 0:
				printf("%d\n",LCT::ask(x,y));
				break;
			case 1:
				LCT::link(x,y);
				break;
			case 2:
				LCT::cut(x,y);
				break;
			case 3:
				LCT::modify(x,y);
		}
	}
	return 0;
}

LCT 应用

呼哧~
废了好半天才打完的板子,LCT一定很有用吧?这一点还是可以肯定的


维护连通性质

这本来是并查集干的活
如果只有加边,并查集得心应手
如果只有删边,倒过来就可以了
如果都有,还是LCT吧……

P2147 [SDOI2008]洞穴勘测

这就很板子了吧?如果联通那么他们所在树的根是相同的


维护链上信息

比如刚才那道模板题

再来一点儿复杂信息的模板题:

P1501 [国家集训队]Tree II

LCT模板+线段树模板2即可,懒标记和翻转标记一起 \(pushdown\) 即可


维护边权信息

可以把边替换成一个点,这个点分别向两端点连边,点权设为边权

B. 树的维护

上一篇:【物理应用】Matlab 二维对流扩散温度场


下一篇:Userspace RCU的使用与原理