动态树 (LCT) 介绍篇

动态树 (LCT) 介绍篇#

简介#

LCT,全名 Link Cut Tree,一般基于 Splay + 实链剖分 来实现

LCT 能够支持许多的操作:

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

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

  • 动态连边/删边

  • 动态维护连通性

  • lca

这些操作均摊时间复杂度是 O(nlogn)

具体证明的link

总而言之,LCT 是一种非常实用的数据结构

就是貌似现在不咋考

基本思路及实现#

Part I 实链剖分#

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

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


Part II Splay#

一棵 LCT 是由很多棵 Splay 构成的

  • Splay 维护了什么?

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

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

而且,每个节点都必须在且仅在一棵 Splay

  • 实边和虚边在 Splay 中是如何体现的?

实边连接了两个同一棵 Splay 的节点,而虚边则是连接了两个不同 Splay 的节点

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

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

这样我们就能够维护每一棵 Splay 的完整性


Part III 核心操作#

这里先说明一下

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

  • access 操作

access(x):让 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的子节点没有实边
}

  • push_down 以及 rev 操作

push_down:下放 x 的子树翻转的懒标记

rev(x):翻转 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;
}

  • makeroot 操作

makeroot(x):让 x 变为原树的根

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

  • findroot 操作

findroot(x):找到 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;
}

  • rootful 操作

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

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

  • split 操作

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

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

  • link 操作

link(x,y):连一条 x-y 的边

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

  • cut 操作

cut(x,y):断开 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);
	}
}

注意:LCTSplay 的操作与原先略有不同


【模板】动态树(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;
		}
	}
}

作者:Into_qwq

出处:https://www.cnblogs.com/into-qwq/p/16512574.html

版权:本作品采用「qwq」许可协议进行许可。

posted @   Into_qwq  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示