其他分享
首页 > 其他分享> > 动态树 $(LCT)$

动态树 $(LCT)$

作者:互联网

目录

动态树 \((LCT)\)

前言

\(\text{LCT}\),全名 \(\text{Link Cut Tree}\),一般基于 \(\text{Splay}\) + 实链剖分 来实现

其独特之处就是可以在树上动态删边加边的同时维护树的各种性质以及信息,并且这些操作均摊是 \(\text{O}(n \log^2 n)\) 的

能够如此高效地维护上述这一操作是大部分数据结构无法做到的

总而言之,\(\text{LCT}\) 是一种非常实用的数据结构

就是貌似现在不咋考

基本思路及实现

\(\text{Part I}\) 实链剖分

实链剖分这个东西其实可以类比重链剖分,我们将某一个儿子的连边划分为实边,而连向其他子树的边划分为虚边

但是在 \(\text{LCT}\) 中这个虚边实边是会动态变化的,我们需要用灵活的 \(\text{Splay}\) 来维护

\(\text{Part II Splay}\)

一棵 \(\text{LCT}\) 是由很多棵 \(\text{Splay}\) 构成的

每一棵 \(\text{Splay}\) 都包含了一条由原树中从上到下深度严格递增的节点构成的链,且 \(\text{Splay}\) 中的节点是按深度排序的

也就是说,每一棵 \(\text{Splay}\) 的中序遍历得到的节点深度依次递增

而且,每个节点都必须在且仅在一棵 \(\text{Splay}\) 内

实边连接了两个同一棵 \(\text{Splay}\) 的节点,而虚边则是连接了两个不同 \(\text{Splay}\) 的节点

由于实链剖分的性质,每个节点必须且仅能向一个儿子连一条实边,而与剩下的所有儿子均连虚边

为了维持树的形状,\(\text{LCT}\) 采用了认父不认子的方法来维护,也就是虚边相连的两个点 \(x,y\) 中,只能有 \(y\) 的父亲为 \(x\) (假设 \(x\) 深度更小),\(x\) 没有 \(y\) 这个儿子

这样我们就能够维护每一棵 \(\text{Splay}\) 的完整性

\(\text{Part III}\) 核心操作

这里先说明一下,\(\text{l(x)}\) 表示 \(x\) 的左儿子,而 \(\text{r(x)}\) 表示 \(x\) 的右儿子

\(\text{access(x)}\):让 \(x\) 到根节点路径上所有链都变为实链

inline void access(int x){//让x->root路径上所有链变成实链,且ch[x][0/1]均不在此实链上
		for(int y=0;x;x=f[y=x]){
			splay(x);
			r(x)=y;//注意要维持splay深度有序
			push_up(x);
		}
		//access的过程实际上是不断把x向上旋到当前splay的根,然后切断原来的实链,将其更新成当前实链
		//这样仍能保证每个节点仅有一条实边,且x->root均为实边
		//最开始会将ch[x][1]赋值成0,这样就能保证x->x的子节点没有实边
	}

【模板】动态树(\(\text{Link Cut Tree}\))

#include<bits/stdc++.h>
using namespace std;

const int N=3e5+5;

inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}

int n,m,st[N];
int f[N],ch[N][2],v[N],s[N],lz[N];

namespace LCT{
	//splay中的节点按深度排序
	//splay的中序遍历后得到的节点的深度依次是从小到大的
	//lct均摊时间复杂度O(nlognlogn)
	#define l(x) ch[x][0]
	#define r(x) ch[x][1]
	inline bool rootful(int x){return l(f[x])!=x&&r(f[x])!=x;}//x和f[x]是否连轻边,即x是否为根
	inline int chk(int x){return r(f[x])==x;}
	inline void push_up(int x){s[x]=s[l(x)]^s[r(x)]^v[x];}
	inline void rev(int x){swap(l(x),r(x));lz[x]^=1;}//反转整棵splay
	inline void push_down(int x){//下放懒标记
		if(!lz[x]) return;
		if(l(x)) rev(l(x));
		if(r(x)) rev(r(x));
		lz[x]=0;
	}
	inline void rotate(int x){
		int y=f[x],z=f[y],k=chk(x),w=ch[x][k^1];
		if(!rootful(y)) ch[z][chk(y)]=x;f[x]=z;//注意这里要判y是否为根
		if(w) f[w]=y;ch[y][k]=w;
		ch[x][k^1]=y,f[y]=x;
		push_up(y),push_up(x);
	}
	inline void splay(int x){//把x转到当前splay的根
		int y=x,top=0,z;
		for(st[++top]=y;!rootful(y);st[++top]=y=f[y]);//暂存当前点->根的路径
		for(;top;push_down(st[top--]));//这样就能从上到下释放懒标记
		while(!rootful(x)){
			y=f[x],z=f[y];
			if(!rootful(y))
				rotate(chk(x)==chk(y)?y:x);
			rotate(x);
		}
	}
	inline void access(int x){//让x->root路径上所有链变成实链,且ch[x][0/1]均不在此实链上
		for(int y=0;x;x=f[y=x]){
			splay(x);
			r(x)=y;//注意要维持splay深度有序
			push_up(x);
		}
		//access的过程实际上是不断把x向上旋到当前splay的根,然后切断原来的实链,将其更新成当前实链
		//这样仍能保证每个节点仅有一条实边,且x->root均为实边
		//最开始会将ch[x][1]赋值成0,这样就能保证x->x的子节点没有实边
	}
	inline void makeroot(int x){//将x变为整棵树的根
		access(x);//连x->root,此时在x->root的路径中x的深度最大
		splay(x);//x变成root
		rev(x);//把当前splay翻转,让x深度最小
	}
	inline int findroot(int x){
		access(x);splay(x);
		for(;l(x);x=l(x)) push_down(x);
		splay(x);//保证复杂度
		return x;
	}
	inline void split(int x,int y){//拉出x-y的路径,让其为一棵splay (y为根)
		makeroot(x);//将x变为根
		access(y);//将x-y联通
		splay(y);//将y转到根
	}
	inline void link(int x,int y){
		makeroot(x);
		if(findroot(y)!=x) f[x]=y;//不在同一子树中,可以连边
	}
	inline void cut(int x,int y){//删x-y的边
		makeroot(x);
		if(findroot(y)==x&&f[y]==x&&!l(y)){
			//y所在的splay根必须为y且y的父亲必须为x
			//y没有左儿子,因为若有左儿子说明dep[x]<dep[ch[y][0]]<dep[y],那x,y之间就没有边
			f[y]=r(x)=0;
			push_up(x);
		}
	}
}

signed main(){
	n=read(),m=read();
	for(int i=1;i<=n;++i) v[i]=read();
	while(m--){
		int op=read(),x=read(),y=read();
		if(op==0){
			LCT::split(x,y);
			cout<<s[y]<<endl;
		}
		if(op==1) LCT::link(x,y);
		if(op==2) LCT::cut(x,y);
		if(op==3){
			LCT::splay(x);//先把x旋到当前splay的根,用来更新懒标记
			v[x]=y;
		}
	}
}

标签:splay,LCT,实链,实边,int,text,Splay,动态
来源: https://www.cnblogs.com/into-qwq/p/16512574.html