Loading

【瞎口胡】Link-Cut Tree

Link Cut Tree 是一种神奇的数据结构。感觉完全不会,写下模板题的学习笔记。

LCT 中,比较重要的一点是将树边划分成实边和虚边。

对于每个非叶节点 \(u\),我们选择一个儿子 \(v\) 作为 \(u\)实儿子\((u,v)\)实边\(u\) 到其它儿子的连边是虚边。

容易发现,实边组成了若干条实链,每个节点恰好在实链中出现了一次(链上只有一个节点仍然算作实链)。

image

上图中,\((1,2,5,7),(3,8),(4,6)\) 是这棵树的实链。

对于每一条实链,我们用一棵 Splay 来维护节点。这棵 Splay 的中序遍历中,节点深度单调递增。

对于上图中的实链剖分,对应的 Splay 森林(注意形态可能不一样,但满足中序遍历节点深度单调递增):

image

上图中绿色的边是虚边。

有一个显然的性质:如果 \((u,v)\) 是实边且 \(u\) 的深度小于 \(v\),那么 \(v\) 是 Splay 中 \(u\) 的儿子。无论 \((u,v)\) 是否是实边,\(v\) 的父亲都指向 \(u\)

Access ~ 点与根的连接

LCT 的核心操作是 access()access(x) 改变了原树的实链剖分,同时构造出一棵中序遍历以根节点开始,以 \(x\) 结束的 Splay。

首先把 \(x\)\(x\) 的实儿子的连边改成虚边,因为要构造的 Splay 中序遍历以 \(x\) 结束。然后不断地从深度较大的 Splay 跳到深度较小的 Splay,并把跳的过程中经过的所有虚边改为实边。

inline void access(int x){
	for(rr int y=0;x;y=x,x=fa(x)){
		splay(x),rc(x)=y,update(x);
	}
}

Makeroot ~ 换根

有了这个操作之后,我们可以来换根。makeroot(x) 的定义是让 \(x\) 成为原树的根。

我们怎么做呢?观察到,不在 \(x\) 到原来根路径上的点不会受到影响。于是我们只需要把 \(x\) 到原来根路径上的点拉出来,这就是 access(x)。然后我们将 \(x\) 旋转上去,这样 \(x\) 只有左子树(LCT 中 Splay 的性质:中序遍历节点深度单调递增)。我们想让 \(x\) 变成根,只需要翻转这棵 Splay。和做文艺平衡树时一样,打上翻转标记就行。

inline void rev(int x){
	if(!x)
		return;
	std::swap(lc(x),rc(x));
	tree[x].tag^=1;
	return;
}
inline void makeroot(int x){
	access(x),splay(x),rev(x);
	return;
}

Findroot ~ 找根

findroot(x) 的定义是找到 \(x\) 所在树的根。因为维护过程中可能会出现森林,于是这个函数对判断连通性很有帮助。

access(x) 之后再 splay(x),按照上文,此时 \(x\) 只有左子树。往左子树一直走,就走到了根。

为了保证复杂度,找到原树的根之后要把根 splay() 成为 Splay 的根。

inline int findroot(int x){
	access(x),splay(x);
	while(lc(x))
		pushdown(x),x=lc(x); // 记得下放翻转标记
	splay(x);
	return x;
}

Split ~ 提取路径

split(x,y) 的定义是拉出 \(x,y\) 在原树上的路径成为一个 Splay。通常要保证 \(x,y\) 连通。

很简单,makeroot(x) 之后在 access(y) 即可。为了保证复杂度,需要 splay(y)

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

link(x,y) 的定义是连接 \(x,y\)。如果 \(x,y\) 已经连通,则无需连接。

也很简单,因为是无根树,所以我们并不在乎 \(x\)\(y\) 的祖孙关系。默认 \(x\) 的父亲变成 \(y\)makeroot(x) 再将 \(x\) 的父亲设为 \(y\) 就行。

如果 \(x\)\(y\) 连通,那么 makeroot(x) 之后, findroot(y) 的结果一定为 \(x\)。特判即可。

inline void link(int x,int y){
	makeroot(x);
	if(findroot(y)==x){
		return;
	}
	fa(x)=y,update(y); // 记得 update
	return;
}

Cut ~ 断边

cut(x,y) 的定义是断掉 \(x,y\) 在原树上的边。如果没有边就不断。

首先用 findroot(y) 判掉 \(x\)\(y\) 不连通的情况。接下来讨论 \(x\)\(y\) 连通(即 findroot(y)==x)时:

findroot(y) 时已经执行了 access(y) 操作(见 findroot() 的实现),因此 \(x\)\(y\) 的路径已经组成了一棵 Splay。此时若 \(x,y\) 之间有边,则:\(x\)\(y\) 的父亲;\(y\) 没有左子树,否则这些点就会插在中序遍历中 \(x\)\(y\) 的中间。

inline void cut(int x,int y){
	makeroot(x);
	if(findroot(y)==x&&fa(y)==x&&!lc(y)){
		rc(x)=fa(y)=0;
		update(x); // 记得 update
	}
	return;
}

模板题 Luogu P3690

题意

给定 \(n\) 个点以及每个点的权值,要你处理接下来的 \(m\) 个操作。

操作有四种,操作从 \(0\)\(3\) 编号。点从 \(1\)\(n\) 编号。

  • 0 x y 代表询问从 \(x\)\(y\) 的路径上的点的权值的 \(\text{xor}\) 和。保证 \(x\)\(y\) 是联通的。
  • 1 x y 代表连接 \(x\)\(y\),若 \(x\)\(y\) 已经联通则无需连接。
  • 2 x y 代表删除边 \((x,y)\),不保证边 \((x,y)\) 存在。
  • 3 x y 代表将点 \(x\) 上的权值变成 \(y\)

\(n,m \leq 3\times 10^5\),值域 \(10^9\)

题解

update(x) 改成维护异或和就好了。

询问操作就是 split 出来之后 Splay 上点 \(y\) 的异或和(因为我们 Splay 过)。

\(1,2\) 操作都是板子。\(3\) 操作的话需要先把 \(x\) splay 上去再修改,否则 \(x\) 在 Splay 上父亲的信息都没有得到更新。

# include <bits/stdc++.h>

const int N=100010,INF=0x3f3f3f3f;

struct Node{
	int son[2];
	int val,sum,tag,fa;
}tree[N];
int sta[N];
inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-')f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}
inline int& fa(int x){
	return tree[x].fa;
}
inline int& lc(int x){
	return tree[x].son[0];
}
inline int& rc(int x){
	return tree[x].son[1];
}
inline void update(int x){
	tree[x].sum=tree[lc(x)].sum^tree[rc(x)].sum^tree[x].val;
	return;
}
inline bool nroot(int x){
	return (lc(fa(x))==x)||(rc(fa(x))==x);
}
inline void rotate(int x){
	if(!nroot(x))
		return;
	int y=fa(x),z=fa(y),k=(rc(y)==x),ns=tree[x].son[!k];
	if(nroot(y))
		tree[z].son[rc(z)==y]=x;
	tree[x].son[!k]=y,tree[y].son[k]=ns;
	if(ns)
		fa(ns)=y;
	fa(y)=x,fa(x)=z;
	update(y),update(x); // 顺序注意 
	return;
}
inline void rev(int x){
	if(!x)
		return;
	std::swap(lc(x),rc(x));
	tree[x].tag^=1;
	return;
}
inline void pushdown(int x){
	if(!tree[x].tag)
		return;
	rev(lc(x)),rev(rc(x)),tree[x].tag=0;
	return;
}
inline void splay(int x){
	int top=0;
	int y=x;
	for(;;){ // 记得这里要先下放标记
		sta[++top]=y;
		if(!nroot(y))
			break;		
		y=fa(y);
	}
	while(top)
		pushdown(sta[top--]);
	while(nroot(x)){
		int y=fa(x),z=fa(y);
		if(nroot(y)){
			((rc(y)==x)==(rc(z)==y))?rotate(y):rotate(x);
		}
		rotate(x);
	}
	return;
}
inline void access(int x){
	for(int y=0;x;y=x,x=fa(x)){
		splay(x),rc(x)=y,update(x);
	}
	return;
}
inline void makeroot(int x){
	access(x),splay(x),rev(x);
	return;
}
inline int findroot(int x){
	access(x),splay(x);
	while(lc(x))
		pushdown(x),x=lc(x);
	splay(x);
	return x;
}
inline void split(int x,int y){
	makeroot(x),access(y),splay(y);
	return;
}
inline void link(int x,int y){
	makeroot(x);
	if(findroot(y)==x){
		return;
	}
	fa(x)=y,update(y);
	return;
}
inline void cut(int x,int y){
	makeroot(x);
	if(findroot(y)==x&&rc(x)==y&&!lc(y)){ // 顺序不能反 
		fa(y)=0,rc(x)=0;
		update(x);
	}
	return;
}
int main(void){
	int n=read(),m=read();
	for(int i=1;i<=n;++i){
		tree[i].sum=tree[i].val=read();
	}
	int x,y,opt;
	while(m--){
		opt=read(),x=read(),y=read();
		switch(opt){
			case 0:{
				split(x,y);
				printf("%d\n",tree[y].sum);
				break;
			}
			case 1:{
				link(x,y);
				break;
			}
			case 2:{
				cut(x,y);
				break;
			}
			case 3:{
				splay(x),tree[x].val=y,update(x);
				break;
			}
		}
	}
	return 0;
}
posted @ 2021-07-22 22:10  Meatherm  阅读(78)  评论(0编辑  收藏  举报