LCT 学习笔记
一:初步认识
LCT 是一种用于解决动态树问题的数据结构。
动态树问题就是在常见的树链剖分题中加入加边和删边操作。
前置知识:Splay 树
二:实链剖分
因为动态树问题可以视为树上问题的扩展版,所以我们还是需要用树链剖分维护树上信息。
在长链剖分、重链剖分和实链剖分中,我们选择实链剖分来解决动态树问题。
实链剖分就是对于每一个节点,任选一个儿子为实儿子,其余的儿子为虚儿子;链接实儿子的边称为实边;链接虚儿子的边称为虚边。一条由实边构成的链叫做实链。
为什么我们会选择实链剖分呢?因为实链剖分最灵活且可变,你会在 LCT 的操作部分体会到这一点。
三:辅助树
动态树问题直接做不好做,因此我们要引入一个叫辅助树的东西(为了区别,我们称原来的树叫原树)。
这里有一些关于辅助树和 LCT 的一些事实:
-
为了方便理解,你可以把辅助树理解为一些 Splay 树;而 LCT 维护的就是一个由辅助树构成的森林;
-
在一颗辅助树中,一个 Splay 树维护原树上一条实链,且满足中序遍历这颗 Splay 数得到的序列对应原树上一条从上到下的路径;
-
在一颗辅助树中,每一颗 Splay 树的根节点不一定为空,它指向在原树中,这颗 Splay 树对应的实链的链顶的父节点,即一条虚边;这类父亲链接满足认父不认子的性质(即父亲节点无法通过访问子节点访问到该节点)。
-
基于上述性质,我们对原树的操作都可以转换为在辅助树上的操作。
-
这里还有一个重要性质:由第二点性质可以得知,对于一颗 Splay 树上的每一个节点,它的左儿子代表它在原树上的父亲,右儿子代表它在原树上的实儿子。
四:LCT 的实现
一些 Splay 树的操作和基本操作
和正常的 Splay 树操作基本相同,但是在细节上有区别。
#define Get(x) (x==ch[fa[x]][1])
//依据“认父不认子”的性质判断是否为 Splay 的根节点
#define Isroot(x) (x!=ch[fa[x]][0] && x!=ch[fa[x]][1])
void push_up(int x){val[x]=val[ch[x][0]]^val[ch[x][1]]^a[x];}
void upd(int x){//打标记
if(!x) return;
swap(ch[x][0],ch[x][1]),tag[x]^=1;
}
void push_down(int x){//下传标记
if(tag[x])
upd(ch[x][0]),upd(ch[x][1]),tag[x]=0;
}
void Update(int x){//从 x 出发下传标记直到 Splay 的根。
if(!Isroot(x)) Update(fa[x]);
push_down(x);
}
void rotate(int x){
int y=fa[x],z=fa[y],id=Get(x);
//这里不同,因为根节点的父亲会指向虚边的另一个端点,因此必须通过性质判断是否为根节点。
if(!Isroot(y)) ch[z][Get(y)]=x;
ch[y][id]=ch[x][id^1],ch[x][id^1]=y;
if(ch[y][id]) fa[ch[y][id]]=y;
fa[y]=x,fa[x]=z;
push_up(y),push_up(x);
}
void Splay(int x){
Update(x);//将沿途会访问到的点的标记下传
for(int y;y=fa[x],!Isroot(x);rotate(x))
if(!Isroot(y))
rotate(Get(x)==Get(y) ? y : x);
}
Access 操作(核心)
Access(x)
的功能是构造一条实链,满足一段为 \(x\) ,另一端为这颗辅助树的根节点。
这个操作是简单的,具体地,从 \(x\) 开始:
-
将当前节点转移到根(因为只有在根节点才能处理虚边);
-
将儿子(原树上的儿子,即辅助树上的右儿子)设为上一次的节点;
-
维护当前节点信息;
-
将当前节点变为当前节点的父亲,回到第一步;
int Access(int x){
int p=0;
for(p=0;x;p=x,x=fa[x])
Splay(x),ch[x][1]=p,push_up(x);
return p;
//这个函数的返回值是这条实链对应的Splay的根节点,保证其父亲为空。
}
至此你已经学完 LCT 的 \(60\%\) 了。
makeroot 操作
makeroot(x)
的功能是将 \(x\) 设为当前辅助树的根。
等价于在原来根到 \(x\) 的实链上,对于每一个点,将父亲指向原来的实儿子,实儿子指向原来的父亲(好好体会一下)。
体现在 Splay 上就是交换左右儿子,用懒标记实现即可。
void makeroot(int x){
x=Access(x),upd(x);//upd 函数的实现在前面
}
spilt 操作
spilt(x,y)
的功能是将 \(x\) 和 \(y\) 放到一个 Splay 中,以维护 \(x\) 到 \(y\) 这条路径。
实现不难想到,直接给代码。
void spilt(int x,int y){
makeroot(x),Access(y),Splay(y);
}
find 操作
find(x)
的功能是找到 \(x\) 在原树中的根。
Access(x)
后,保证原树中的根就在 \(x\) 所在的 Splay 树中(因为这个节点始终没有离开过辅助树的根所在的 Splay 树),因此只需要不停跳左儿子即可(也就是在原树上不停跳父亲)。
int find(int x){
Access(x),Splay(x);
push_down(x);
while(ch[x][0]) x=ch[x][0],push_down(x);
Splay(x);
return x;
}
link 操作
link(x,y)
的功能是连边 \((x,y)\)。
只需要把 \(x\) 搞到树根,并且当 Splay 的根就可以随便搞了。(当然 \(x\) 和 \(y\) 不能在同一个树中)
void link(int x,int y){
if(find(x)==find(y)) return;
makeroot(x),Splay(x);
fa[x]=y;
//因为没有连实边,所以不需要维护节点信息,也不需要Splay(y)
}
cut 操作
cut(x,y)
的功能是删边 \((x,y)\)。
先 spilt(x,y)
,然后就可以随便搞了。(当然得有边)
void cut(int x,int y){
makeroot(x),Access(y),Splay(y);
//这个判断条件的理解:在原树上,y的父亲是x,且x没有其他实儿子。
if(ch[y][0]==x && !ch[x][1])
fa[x]=ch[y][0]=0;
}