Link Cut Tree 学习笔记
Link Cut Tree
这里推荐另一位大佬的博客,这篇博客对 LCT 进行了详细的讲解。Link
本篇博客仅用于个人学习记录,可能有的地方写的不够细致准确,还请谅解 uwu;如有谬误,欢迎指出。
引入:
Link Cut Tree (以下简称 LCT)是一种用来维护动态树的数据结构,通俗地讲,LCT 可以做到把一棵树拆成数条链,并对这些链进行较为灵活的操作。
思想&实现
LCT 本质上是一片 Splay 森林,关于为什么用 Splay,因为 Splay 的特点就是非常灵活,不需要对根有特别要求。
对于这片 Splay 森林,其中的每一个节点都对应原树上一个真实节点。我们把原树的边划分为实边与虚边,那么每一个由实边构成的链就对应一颗 Splay,而 Splay 的中序遍历维护链上节点由浅至深的遍历顺序。那么虚边呢?对于原树上虚边相连的两个节点,在 Splay 森林中体现的是将子节点所在的 Splay 的根的父亲设为父节点在其 Splay 中对应的节点。注意,这里的父子节点都指的是原树上的节点。
isroot 函数
那么,我们如何来区分一个节点是否为根呢?因为根和其父节点不属于同一棵 Splay,所以我们只需要判断一个节点的父亲的左右儿子是否有该节点即可,这也是我们要用到的第一个函数:
struct node{
int fa, ch[2];
bool rev_tag;//翻转标记,下文会用到
int lazy;//需要维护信息的懒标记
int val, siz, var;//维护的信息,子树大小,该点的值
}tr[N];
#define ls(x) tr[x].ch[0]
#define rs(x) tr[x].ch[1]
#define fa(x) tr[x].fa
inline bool isroot(int x){
return ls(fa(x))!=x && rs(fa(x))!=x;
}
Splay
然后就是正常的 Splay 部分。还是注意这里根的判别方式。
inline void reverse(int x){
swap(ls(x), rs(x));
tr[x].rev_tag ^=1;
}//下面会用到
void push_down(int x){
if(tr[x].rev_tag){
if(ls(x)) reverse(ls(x));
if(rs(x)) reverse(rs(x));
tr[x].rev_tag = 0;
}
//另一组需要维护的东东
}
void push_up(int x){
tr[x].siz = tr[tr[x].ch[0]].siz+tr[tr[x].ch[1]].siz+1;
}
void rotate(int x){
int y = tr[x].fa, z = tr[y].fa;
int k1 = (tr[z].ch[1] == y), k2 = (tr[y].ch[1] == x);
if(!isroot(y)) tr[z].ch[k1] = x;
tr[x].fa = z;
tr[y].ch[k2] = tr[x].ch[k2^1];
tr[tr[x].ch[k2^1]].fa = y;
tr[x].ch[k2^1] = y;
tr[y].fa = x;
push_up(y), push_up(x);
}
void update(int x){
if(!isroot(x)) update(fa(x));
push_down(x);
}//对树进行操作之前,必须把懒标记从头至尾全部下放。
void splay(int x){
update(x);
while(!(isroot(x))){//不同点
int y = tr[x].fa, z = tr[y].fa;
if(!isroot(y)){
((tr[z].ch[0]==y)!=(tr[y].ch[0]==x))?rotate(x):rotate(y);
}
rotate(x);
}
}
access 函数
这个函数的含义是,把当前节点到原树上的根之间的路径变为实边,原来与这条路径上节点相连的非路径边都变成虚边。这样我们就能够抽出一条链来。
inline int access(int x){
int t = 0;
for(t = 0; x; t = x, x = fa(x)){
splay(x), rs(x) = t, push_up(x);
}
return t;//返回最后的根。
}
make_root 函数
这个函数用来转换原树的根。我们来考虑转换根后会发生的变化,发现只会将当前节点到根路径上所有节点的父子关系翻转,也就是,把这条链的先序遍历翻转,也就是区间翻转。这时候就用上之前写的 reverse 函数了。
inline void make_root(int x){
x = access(x);
reverse(x);
}
find_root 函数
既然原树上的根是不确定的,那么我们如何定位一个节点所在原树的根呢?答案是先 access 一次,然后一直向左子树走,根据二叉搜索树的性质,最左侧的点一定是深度最浅的点,也就是根。
inline int find_root(int x){
x = access(x), push_down(x);
while(ls(x)) x = ls(x), push_down(x);
splay(x); return x;
}
link, cut, split 函数
总算见到主角辣(
link 函数很好理解,就是把两棵原来不相连的树链接起来,注意要判断原来是否是同一棵树。
inline void link(int x, int y){
make_root(x);
if(find_root(y)!=x) splay(x), tr[x].fa = y;
}
cut 就是 link 的反向操作:切断两个节点之间的边,让他们所在的树分裂为两棵树。注意这里不仅要判断原来是否在一棵树,还要判断在原来树上是否有边相连(不能把相隔很远的两个节点断开)。
inline void cut(int x, int y){
make_root(x);
if(find_root(y) == x && fa(y) == x && !ls(y)){
tr[y].fa = tr[x].ch[1] = 0, push_up(x);
}
}
split 就是在原树上分离出两个节点之间的路径。
inline void split(int x, int y){
make_root(x);
access(y);
splay(y);
}