Loading

动态树 $\text{(LCT)}$ 介绍篇

动态树 \(\text{(LCT)}\) 介绍篇

简介

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

\(\text{LCT}\) 能够支持许多的操作:

  • 查询/修改树上某条链的信息

  • 将任意一点变为原树的根

  • 动态连边/删边

  • 动态维护连通性

  • \(\text{lca}\)

这些操作均摊时间复杂度是 \(\text{O}(n \log n)\)

\(\to\text{具体证明的link}\leftarrow\)

总而言之,\(\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{Splay}\) 的节点,而虚边则是连接了两个不同 \(\text{Splay}\) 的节点

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

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

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


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

这里先说明一下

名称 含义
\(\text{l(x)/ch[x][0]}\) \(\text{x}\) 的左儿子
\(\text{r(x)/ch[x][1]}\) \(\text{x}\) 的右儿子
\(\text{f[x]}\) \(\text{x}\) 的父亲
\(\text{v[x]}\) \(\text{x}\) 的权值
\(\text{s[x]}\) \(\text{Splay}\)\(\text{x}\) 所有子树的异或和
\(\text{lz[x]}\) \(\text{Splay}\)\(\text{x}\) 是否要反转子树的懒标记

  • \(\text{access}\) 操作

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

inline void access(int x){
	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{push}\_\text{down}\) 以及 \(\text{rev}\) 操作

\(\text{push}\_\text{down}\):下放 \(\text x\) 的子树翻转的懒标记

\(\text{rev(x)}\):翻转 \(\text x\) 的子树

inline void rev(int x){swap(l(x),r(x));lz[x]^=1;}
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;
}

  • \(\text{makeroot}\) 操作

\(\text{makeroot(x)}\):让 \(\text x\) 变为原树的根

inline void makeroot(int x){
	access(x);//连x->root,此时在x->root的路径中x的深度最大
	splay(x);//x变成root
	rev(x);//把当前splay翻转,让x深度最小
	//x深度最小了,那么x此时就是原树的根
}

  • \(\text{findroot}\) 操作

\(\text{findroot(x)}\):找到 \(\text x\) 所在原树的树根

其主要用于判断两点的连通性

inline int findroot(int x){
	access(x);splay(x);
	for(;l(x);x=l(x)) push_down(x);
	//找根的时候不能保证splay中到根的路径上的翻转标记全push_down了
	splay(x);
	return x;
}

  • \(\text{rootful}\) 操作

\(\text{rootful(x)}\):判断 \(\text x\) 是否为某一棵 \(\text{splay}\) 的根

inline bool rootful(int x){
	return l(f[x])!=x&&r(f[x])!=x;
}//x和f[x]是否连轻边,即x是否为根

  • \(\text{split}\) 操作

\(\text{split(x,y)}\):把 \(\text{x-y}\) 的路径拉出来变成一棵以 \(\text y\) 为根的 \(\text{splay}\)

inline void split(int x,int y){
	makeroot(x);//将x变为根
	access(y);//将x-y联通
	splay(y);//将y转到根
	//这样做完我们就可以直接访问y来获得x-y路径的信息
}

  • \(\text{link}\) 操作

\(\text{link(x,y)}\):连一条 \(\text{x-y}\) 的边

inline void link(int x,int y){
	makeroot(x);
	if(findroot(y)!=x) f[x]=y;//不在同一子树中,可以连边
}

  • \(\text{cut}\) 操作

\(\text{cut(x,y)}\):断开 \(\text{x-y}\) 的边

inline void cut(int x,int y){
	makeroot(x);
	if(findroot(y)==x&&f[y]==x&&!l(y)){
		//y所在的splay根必须为y且y的父亲必须为x
		//y没有左儿子,因为若有左儿子说明dep[x]<dep[c[y][0]]<dep[y],那x,y之间就没有边
		f[y]=r(x)=0;
		push_up(x);
	}
}

注意:\(\text{LCT}\)\(\text{Splay}\) 的操作与原先略有不同


【模板】动态树(\(\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{
	#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;}
	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;}
	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){
		for(int y=0;x;x=f[y=x]){
			splay(x);
			r(x)=y;
			push_up(x);
		}
	}
	inline void makeroot(int x){
		access(x);
		splay(x);
		rev(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){
		makeroot(x);
		access(y);
		splay(y);
	}
	inline void link(int x,int y){
		makeroot(x);
		if(findroot(y)!=x) f[x]=y;
	}
	inline void cut(int x,int y){
		makeroot(x);
		if(findroot(y)==x&&f[y]==x&&!l(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;
		}
	}
}
posted @ 2022-07-23 22:25  Into_qwq  阅读(25)  评论(0编辑  收藏  举报