[模板] 动态树 LCT

今天终于把这东西学会了。

比大模拟还难写。

概念类

  • Link-Cut-Tree 是用来解决 动态树 问题的一种数据结构。

  • 类似于轻重链剖分,这里使用 虚实链剖分 ,同时利用 Splay 灵活多变的性质来维护动态操作,其中 每一条实链都用一棵 Splay 来维护。(也叫作 Auxiliary Tree(辅助树)

    • 基本思想可以概括为:“只认父亲,不认儿子”
    • 也就是说对于任意一个非根节点,记录它的父亲和它的 唯一的重儿子

辅助树的一些注意要点:

  • 分清”假根“和“真根”:

    • 假根:辅助树的根。
    • 真根:整棵原图树的根。
  • 辅助树的键值为 每个节点的深度,也就是说辅助树的中序遍历的节点的深度是 严格单调递增 的。

  • 对于辅助树的根 \(rt\)\(rt\) 与辅助树内的中序遍历的第一个点在原树上的 father 连单向边(也是虚边),这样辅助树和原图就连接起来了。

原树与辅助树的区别:@Candy

[模板] 动态树 LCT_git

具体操作

Splay 基本操作

需要考虑懒标记的影响。

判断是不是假根也和原来有区别:

inline bool nroot(int x){
	return son[fa[x]][0]==x || son[fa[x]][1]==x;//只认重儿子,也是和普通Splay的区别
}
inline void rotate(int x){
	int y=fa[x],z=fa[y];
	bool rsx=rson(x),rsy=rson(y);
	if(nroot(y))son[z][rsy]=x;
	son[y][rsx]=son[x][!rsx];
	son[x][!rsx]=y;
	fa[son[y][rsx]]=y;fa[y]=x;fa[x]=z;
	pushup(y);
}
void splay(int x){
	top=0;int y=x,z=0;
	stk[++top]=y;
	while(nroot(y))stk[++top]=fa[y],y=fa[y];//因为是从上到下,所以用栈
	while(top)pushdown(stk[top--]);//warning!
	while(nroot(x)){
		y=fa[x];
		if(nroot(y))rotate((rson(x)^rson(y))?x:y);
		rotate(x);
	}
	pushup(x);
}

access

核心操作:把一个点到树的根的树链打通为实链。

本质上还是一个 Splay 合并的过程,或者说是一个不断丢弃右儿子的过程(因为向上合并,深度递减,只能接到右儿子上)。

这里注意:打通后的 Splay 的根节点是无法确定的,并不是一条链的结构,因为辅助树右子树丢掉了,但左子树也许存在。

这也就是说:尽量每次操作后如果有必要的话,都 Splay 当前节点调整树的结构,维持其复杂度。

至于为什么会WA,我也不清楚

具体来说做法就是想上找 \(fa\) 然后合并就行了。

void access(int x){//打通到(真)根
	for(int y=0;x;y=x,x=fa[x]){
		splay(x);son[x][1]=y;pushup(x);
	}
}

makeroot

让当前点变成原树的根节点。

可以先打通到根的路径,然后让 \(x\) 成为辅助树根节点,然后进行 颠倒+打 tag 操作。

inline void makeroot(int x){//换(真)根
	access(x);splay(x);
	pushr(x);
}

findroot

找到当前节点所在 连通块 的树的根。(因为 LCT 经典应用就是维护森林)

也是先打通,再 \(splay(x)\),然后一直往左子树走找深度最小的点,最后得到的点就是根节点。

注意处理 tag

至于最后为什么要 \(splay\) 一下,网上说法大不相同。

UPdated on 2021/7/11

之所以最后要 \(splay\) 一下是因为在 cut 操作的时候会出问题,要保证 findroot 的结点的 fa 最后不能为 0 。

int findroot(int x){//找(真)根
	access(x);splay(x);
	while(son[x][0])pushdown(x),x=son[x][0];
	splay(x);//调整形态,保证复杂度 ?? 也许大概率是因为 pushdown 的问题。
	return x;
}

split

把一条链 \((x,y)\) 拎出来,或者说这才是应用的操作。

可以先让 \(x\) 成为根节点,然后打通 \(y\) 到根节点的路径,最后 \(splay(y)\)

inline void split(int x,int y){
	makeroot(x);access(y);splay(y);//y是根
}

link

特判有没有在一个连通块内即可。

inline void link(int x,int y){
	makeroot(x);
	if(findroot(y)!=x)fa[x]=y;
}

cut

inline void cut(int x,int y){
	makeroot(x);
	if(findroot(y)==x && fa[y]==x && !son[y][0]){
		fa[y]=son[x][1]=0;pushup(x);
	}
}

总代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
template <typename T>
inline T read(){
	T x=0;char ch=getchar();bool fl=false;
	while(!isdigit(ch)){if(ch=='-')fl=true;ch=getchar();}
	while(isdigit(ch)){
		x=(x<<3)+(x<<1)+(ch^48);ch=getchar();
	}
	return fl?-x:x;
}
#define read() read<int>()
const int maxn = 1e5 + 10;
int n,m,val[maxn];
int fa[maxn],son[maxn][2],s[maxn],stk[maxn],top=0;
bool rev[maxn];
inline bool nroot(int x){
	return son[fa[x]][0]==x || son[fa[x]][1]==x;//只认重儿子,也是和普通Splay的区别
}
inline void pushr(int x){
	int tmp=son[x][0];son[x][0]=son[x][1];son[x][1]=tmp;
	rev[x]^=1;
}
inline void pushup(int x){
	s[x]=s[son[x][0]]^s[son[x][1]]^val[x];
}
inline void pushdown(int x){
	if(!rev[x])return ;
	if(son[x][0])pushr(son[x][0]);
	if(son[x][1])pushr(son[x][1]);rev[x]=0;
}
inline bool rson(int x){return x==son[fa[x]][1];}
inline void rotate(int x){
	int y=fa[x],z=fa[y];
	bool rsx=rson(x),rsy=rson(y);
	if(nroot(y))son[z][rsy]=x;
	son[y][rsx]=son[x][!rsx];
	son[x][!rsx]=y;
	fa[son[y][rsx]]=y;fa[y]=x;fa[x]=z;
	pushup(y);
}
void splay(int x){
	top=0;int y=x,z=0;
	stk[++top]=y;
	while(nroot(y))stk[++top]=fa[y],y=fa[y];
	while(top)pushdown(stk[top--]);//warning!
	while(nroot(x)){
		y=fa[x];
		if(nroot(y))rotate((rson(x)^rson(y))?x:y);
		rotate(x);
	}
	pushup(x);
}
void access(int x){//打通到(真)根
	for(int y=0;x;y=x,x=fa[x]){
		splay(x);son[x][1]=y;pushup(x);
	}
}
inline void makeroot(int x){//换(真)根
	access(x);splay(x);
	pushr(x);
}
int findroot(int x){//找(真)根
	access(x);splay(x);
	while(son[x][0])pushdown(x),x=son[x][0];
	splay(x);//调整形态 
	return x;
}
inline void split(int x,int y){
	makeroot(x);access(y);splay(y);//y是根
}
inline void link(int x,int y){
	makeroot(x);
	if(findroot(y)!=x)fa[x]=y;
}
inline void cut(int x,int y){
	makeroot(x);
	if(findroot(y)==x && fa[y]==x && !son[y][0]){
		fa[y]=son[x][1]=0;pushup(x);
	}
}
int main(){
	n=read();m=read();
	for(int i=1;i<=n;i++)val[i]=read();
	while(m--){
		int op=read(),x=read(),y=read();
		if(op==0)split(x,y),printf("%d\n",s[y]);
		if(op==1)link(x,y);
		if(op==2)cut(x,y);
		if(op==3)splay(x),val[x]=y;
	}
	return 0;
}

LCA

比如求 \((x,y)\) 的 LCA 。

可以先打通 \(x\) 到根节点的路径,再把辅助树的根节点设为 \(x\)

这样最后再让 \(y\) 打通上去,当它跳到 \(LCA\) 的时候,因为要舍弃右子树,所以 \(LCA\)\(x\) 这一段路径就不存在了,\(fa[LCA]=0\),最后 \(access\)\(y\) 就是 \(LCA\)

[模板] 动态树 LCT_git_02