LinkCutTree LCT

更新日志 2025/02/14:开工。

2025/02/28:修改细节。


概念

LCT 可以解决动态树下树链信息的维护。

动态树的意思是可以动态割边、连边。

首先我们将讲解整体思路,然后对各个函数依次介绍。

LCT 的均摊复杂度为 \(O(n\log n)\) 的,证明复杂,所以这里全部掠过,保证复杂度的操作都会提出。

实现

维护

由于动态的割边与连边,所以 LCT 维护的实际上是一片森林。

对于每一棵原树,我们对其进行实链剖分。类似于重链剖分,但实链剖分的实链与虚链是我们按需要决定的。

此外,对于每一棵原树,我们维护一棵辅助树。每一棵辅助树由多棵 Splay 组成,每棵 Splay 维护原树中一条实链。

每棵 Splay 中序遍历的顺序是原树中的深度顺序,由浅到深。

对于原树中每一条虚链,我们采取“认父不认子”的维护方式,对于其子节点记录其父节点,但其父节点不计这个子节点。这样可以快速判断这条链是实链还是虚链。

下面我们对每个函数进行详细讲解。

函数

信息维护函数

用于维护树链信息。

在 Splay 上,我们对于每个节点维护其子树信息,那么这棵 Splay 的根节点就维护了这条实链的信息。

pushup pushdown

这里以维护链异或和为例。

同时,我们必须维护一个子树翻转信息,这将会在后面 makeroot 函数中用到。

题目中额外的信息维护,全部写在这里就可以。

	void pushup(int x){
		sum[x]=sum[son[x][0]]^val[x]^sum[son[x][1]];
	}
	void pushdown(int x){
		if(rev[x]){
			if(son[x][0])swap(son[son[x][0]][0],son[son[x][0]][1]),rev[son[x][0]]^=1;
			if(son[x][1])swap(son[son[x][1]][0],son[son[x][1]][1]),rev[son[x][1]]^=1;
			rev[x]^=1;
		}
	}

update

这个函数用于传递一整条链(实际上是当前点到链头)的标记,方便后续对链的操作。

isroot 函数用于判断是否为当前 Splay 根节点,具体实现后面马上就会讲到。

递归更新即可。

	void update(int x){
		if(!isroot(x))update(fa[x]);
		pushdown(x);
	}

结构信息函数

isroot

用于判断这个节点是否为当前 Splay 根节点。由于虚链认父不认子,所以我们只需要判定这个节点是否为父节点的子节点之一即可。

	#define isroot(x) (x!=son[fa[x]][0]&&x!=son[fa[x]][1])

get

用于获取当前节点为父节点的左右子节点。没啥好说的。

	#define get(x) (x==son[fa[x]][1])

Splay 函数

由于实链是通过 Splay 维护的,所以会用到一些 Splay 的函数,在这里一并讲解。

rotate

用于把当前节点向上旋转。听起来略微抽象。

借图 OI-Wiki:

\(x\) 为当前节点,\(y\) 为其 Splay 内父节点。上面分别是左右节点的情况。

我们以情况 \(1\) 为例,情况 \(2\) 同理。

首先,为了保证中序遍历顺序不变,我们将 \(y\) 的左子节点设为 \(x\) 的右子节点。如果原来存在左子节点,那么把它变成虚链即可,不用特别处理。

然后,我们把 \(y\) 设作 \(x\) 的右子节点,再把 \(y\) 原来的父节点设作 \(x\) 的父节点。

旋转之后,我们显然要更新节点信息,从下往上更新即可,也就是先 \(y\)\(x\)

需要注意操作的顺序。

	void rotate(int x){
		int y=fa[x],z=fa[y],k=get(x);
		if(!isroot(y))son[z][get(y)]=x;
		son[y][k]=son[x][k^1],fa[son[x][k^1]]=y;
		son[x][k^1]=y,fa[y]=x,fa[x]=z;
		pushup(y);pushup(x);
	}

splay

这个操作用于把一个节点旋转到其所在 Splay 的根节点,同时保证均摊复杂度。

复杂度证明这里暂且略去,如有需要,请自行查询 OI-Wiki。

在旋转操作之前,我们先把一路上的标记全部传递下来,也就是 update 一遍。

zig

当前节点父节点为根的情况,直接向上 rotate 即可。

zig-zig

当前节点父节点是同一侧子节点的情况,具体的:

我们先 rotate(y),然后 rotate(x) 即可。

zig-zag

当前节点父节点不是同一侧子节点的情况,具体的:

我们先 rotate(x),然后 rotate(x) 即可。

	void splay(int x){
		update(x);
		for(int f;f=fa[x],!isroot(x);rotate(x))
			if(!isroot(f))rotate(get(x)==get(f)?f:x);
	}

核心函数

这两个函数是 LCT 最核心的函数。

access

在原树上将当前点到原树根节点拉出一条实链。

对于我们经过的每一个节点,我们首先将其旋转到其 Splay 的根节点,然后将其右子节点设为上一棵 Splay 的根节点(如果这是第一步,就设为空,因为要单独拉出一条实链,所以要把下面的部分截掉)(旋转到根节点和放右子节点是为了维护中序深度递增)。

由于链发生了变化,别忘了 \(pushup\)

我们最后走到的节点显然就是原树的根节点。我们这里顺便返回一下辅助树的新根节点,方便 makeroot 操作。

	int access(int x){
		int p;
		for(p=0;x;p=x,x=fa[x]){
			splay(x);
			son[x][1]=p;
			pushup(x);
		}
		return p;
	}

makeroot

为了方便维护中序深度递增,我们有时候会需要把某一个节点设为原树的根节点,比如连边的时候。

首先我们拉出当前节点到原树根的实链,也就是 access 一遍。

其余节点的相对深度关系都没有变化,因此我们只需要反转这条实链的深度关系即可。

所以我们考虑反转中序顺序,也就是反转每个节点的左右子节点。

	void makeroot(int x){
		x=access(x);
		swap(son[x][0],son[x][1]);
		rev[x]^=1;
	}

常用函数

find

用于查找所在原树的根。

我们将当前节点到原根拉一条实链,然后这条实链中序第一个遍历到的就是根节点。

具体的,我们先把当前节点转到根节点,然后一直往左走就行。

为了维护复杂度,把找到的根节点转到这条实链的根节点。

	int find(int x){
		access(x);
		splay(x);
		pushdown(x);
		while(son[x][0])x=son[x][0],pushdown(x);
		splay(x);
		return x;
	}

split

用于单独拉出 \(x\)\(y\) 的链。前提是两者连通。

我们只需要先把 \(x\) 设为根节点,然后拉通 \(y\) 到根的链即可。

为了方便 cut 操作,我们通常把 \(y\) 转到 Splay 根节点。

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

连接两个节点,前提是两者不连通。

我们令为 \(x\) 连向 \(y\),为了维护深度关系,我们先把 \(x\) 设作原树根节点。

然后,我们把 \(x\) 转到 Splay 的根节点,并连一条向 \(y\) 的虚链即可。

	void link(int x,int y){
		makeroot(x);
		splay(x);
		fa[x]=y;
	}

cut

断开一条边,前提是有这条边。

提前存好边就行。有一个根据性质的判断方法,我不会。

我们先取出连接二者的实链,由于二者有边相连,所以二者在实链上必然相邻。

直接双向断开即可。

	void cut(int x,int y){
		split(x,y);
		son[y][get(x)]=fa[x]=0;
	}

change

修改点权操作。为了方便维护子树信息,我们先把要修改的点转到 Splay 根节点,然后直接改,并更新子树信息。

	void change(int x,int v){
		splay(x);
		val[x]=v;
		pushup(x);
	}

模板

以维护异或和为例。

struct LCT{
	int son[N][2],fa[N];
	int rev[N];
	int val[N],sum[N];
	#define isroot(x) (x!=son[fa[x]][0]&&x!=son[fa[x]][1])
	#define get(x) (x==son[fa[x]][1])
	void pushup(int x){
		sum[x]=sum[son[x][0]]^val[x]^sum[son[x][1]];
	}
	void pushdown(int x){
		if(rev[x]){
			if(son[x][0])swap(son[son[x][0]][0],son[son[x][0]][1]),rev[son[x][0]]^=1;
			if(son[x][1])swap(son[son[x][1]][0],son[son[x][1]][1]),rev[son[x][1]]^=1;
			rev[x]^=1;
		}
	}
	void update(int x){
		if(!isroot(x))update(fa[x]);
		pushdown(x);
	}
	void rotate(int x){
		int y=fa[x],z=fa[y],k=get(x);
		if(!isroot(y))son[z][get(y)]=x;
		son[y][k]=son[x][k^1],fa[son[x][k^1]]=y;
		son[x][k^1]=y,fa[y]=x,fa[x]=z;
		pushup(y);pushup(x);
	}
	void splay(int x){
		update(x);
		for(int f;f=fa[x],!isroot(x);rotate(x))
			if(!isroot(f))rotate(get(x)==get(f)?f:x);
	}
	int access(int x){
		int p;
		for(p=0;x;p=x,x=fa[x]){
			splay(x);
			son[x][1]=p;
			pushup(x);
		}
		return p;
	}
	void makeroot(int x){
		x=access(x);
		swap(son[x][0],son[x][1]);
		rev[x]^=1;
	}
	int find(int x){
		access(x);
		splay(x);
		pushdown(x);
		while(son[x][0])x=son[x][0],pushdown(x);
		splay(x);
		return x;
	}
	void link(int x,int y){
		makeroot(x);
		splay(x);
		fa[x]=y;
	}
	void split(int x,int y){
		makeroot(x);
		access(y);
		splay(y);
	}
	void cut(int x,int y){
		split(x,y);
		son[y][get(x)]=fa[x]=0;
	}
	void change(int x,int v){
		splay(x);
		val[x]=v;
		pushup(x);
	}
}lct;

例题

LG3690

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

typedef long long ll;
typedef unsigned long long ull;
typedef __int128 i128;
typedef double db;
typedef long double ld;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
template <typename Type>
using vec=vector<Type>;
template <typename Type>
using grheap=priority_queue<Type>;
template <typename Type>
using lrheap=priority_queue<Type,vector<Type>,greater<Type> >;
#define fir first
#define sec second
#define pub push_back
#define pob pop_back
#define puf push_front
#define pof pop_front
#define chmax(a,b) a=max(a,b)
#define chmin(a,b) a=min(a,b)
#define rep(i,x,y) for(int i=(x);i<=(y);i++)
#define per(i,x,y) for(int i=(x);i>=(y);i--)
#define repl(i,x,y) for(int i=(x);i<(y);i++)
#define file(f) freopen(#f".in","r",stdin);freopen(#f".out","w",stdout);

const int inf=0x3f3f3f3f;
const ll INF=0x3f3f3f3f3f3f3f3f;
const int mod=/*1e9+7*/998244353;

const int N=1e5+5;

struct LCT{
	int son[N][2],fa[N];
	int rev[N];
	int val[N],sum[N];
	#define isroot(x) (x!=son[fa[x]][0]&&x!=son[fa[x]][1])
	#define get(x) (x==son[fa[x]][1])
	void pushup(int x){
		sum[x]=sum[son[x][0]]^val[x]^sum[son[x][1]];
	}
	void pushdown(int x){
		if(rev[x]){
			if(son[x][0])swap(son[son[x][0]][0],son[son[x][0]][1]),rev[son[x][0]]^=1;
			if(son[x][1])swap(son[son[x][1]][0],son[son[x][1]][1]),rev[son[x][1]]^=1;
			rev[x]^=1;
		}
	}
	void update(int x){
		if(!isroot(x))update(fa[x]);
		pushdown(x);
	}
	void rotate(int x){
		int y=fa[x],z=fa[y],k=get(x);
		if(!isroot(y))son[z][get(y)]=x;
		son[y][k]=son[x][k^1],fa[son[x][k^1]]=y;
		son[x][k^1]=y,fa[y]=x,fa[x]=z;
		pushup(y);pushup(x);
	}
	void splay(int x){
		update(x);
		for(int f;f=fa[x],!isroot(x);rotate(x))
			if(!isroot(f))rotate(get(x)==get(f)?f:x);
	}
	int access(int x){
		int p;
		for(p=0;x;p=x,x=fa[x]){
			splay(x);
			son[x][1]=p;
			pushup(x);
		}
		return p;
	}
	void makeroot(int x){
		x=access(x);
		swap(son[x][0],son[x][1]);
		rev[x]^=1;
	}
	int find(int x){
		access(x);
		splay(x);
		pushdown(x);
		while(son[x][0])x=son[x][0],pushdown(x);
		splay(x);
		return x;
	}
	void link(int x,int y){
		makeroot(x);
		splay(x);
		fa[x]=y;
	}
	void split(int x,int y){
		makeroot(x);
		access(y);
		splay(y);
	}
	void cut(int x,int y){
		split(x,y);
		son[y][get(x)]=fa[x]=0;
	}
	void change(int x,int v){
		splay(x);
		val[x]=v;
		pushup(x);
	}
}lct;

int n,m;
set<pii> st;

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	int v;
	rep(i,1,n)cin>>v,lct.change(i,v);
	int op,x,y;
	rep(i,1,m){
		cin>>op>>x>>y;
		if(op==0)lct.split(x,y),cout<<lct.sum[y]<<'\n';
		if(op==1)if(lct.find(x)!=lct.find(y))lct.link(x,y),st.insert({min(x,y),max(x,y)});
		if(op==2)if(st.count({min(x,y),max(x,y)}))lct.cut(x,y),st.erase({min(x,y),max(x,y)});
		if(op==3)lct.change(x,y);
	}
	return 0;
}
posted @   LastKismet  阅读(4)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示