动态树 $\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;
}
}
}