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); }