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 函数里)

这些就是核心函数了,对于询问,常形如“从 xy 的路径上的 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. 树的维护

posted @   y_cx  阅读(53)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示