Link-Cut Tree
Link-Cut Tree(动态树)
本文章中的部分代码和思路来自于 Link Cut Tree - OI Wiki。
定义
LCT 用来解决动态树的问题,即一张图任意时刻一定是一个森林,每次可以删除一条边或者加入一条边,并且还要在线维护两个点之间路径的信息,于是 LCT 便应运而生了。
辅助树
首先 LCT 有自己的一套轻重链剖分关系,它并不像树链剖分那样是选择重儿子延伸重链,而是选择他最近访问过的儿子延伸重链,像全局平衡二叉树那样,每条重链会额外维护一个 Splay(如果是 Treap 时间复杂度不优秀),然后存储若干信息,其中 Splay 根节点的父亲就是这条重链链头的父亲,这条边是虚边,其他边都是实边。
最开始 LCT 每个点自己属于自己一个 Splay,即所有边都是轻边。
操作
判断根节点-Isroot
这个函数特别简单,它的意思是判断 $x$ 是不是它所在 Splay 的根节点,因为 Splay 的根节点的父亲是这个 Splay 所代表重链的链头的父亲,根据上述表述,可以写出:
1 | // 在前面我们已经说过,LCT 具有 如果一个儿子不是实儿子,他的父亲找不到它的性质 |
下放标记-Update
从上到下下放 $p$ 的所有祖先的标记,很简单:
1 | // 从上到下一层一层 pushDown 即可 |
旋转-Splay
这里的 rotate
函数和 splay
函数都有些许不同,splay
函数中要先把 $x$ 的所有祖先的标记都下放了之后才能开始旋转,然后 rotate
函数中当 $y$ 不是根节点的时候才能改变 $x,z$ 两者之间的关系。
1 |
|
访问-Access
这个操作是最重要的操作,它可以让一个节点 $x$ 的重链链头成为根节点,相当于把 $x$ 到根节点上的所有边变成重边,原来处在 $x$ 到根路径上的点的重边都删去。
函数可以写成下面的形式,如果有 tag
一定要在 Splay 函数中下放。
1 | // Access 是 LCT |
换根-Makeroot
首先 access(x)
,然后把 $x$ 所在平衡树做一个文艺平衡树中的区间翻转即可。
1 | void makeRoot(int p) { |
连接-Link
Link 操作只需要把 $x$ 旋转到根节点(树的根节点以及其 Splay 的根节点),然后直接连接到 $y$ 上即可。
1 | void Link(int x, int y) { |
分裂-Split
这个函数的意义在于拿出一棵 Splay 只维护 $x,y$ 这条链的信息,于是这样就可以了:
1 | void Split(int x, int y) { |
断边-Cut
首先判断合不合法,然后利用 split 函数把 $x \to y$ 这条链拿出来,断开即可,注意 pushup
和清空的细节问题:
1 | void Cut(int x, int p) { makeRoot(x), Access(p), Splay(p), tr[p].ch[0] = f[x] = 0; } |
查询-Find
这个函数是查询 $x$ 在原树上的根,首先 access 一下,然后暴力 splay,最后暴力跳左儿子即可,根据 splay 的性质,最后要 splay 一下根节点复杂度才有保证。(记得 pushdown
)
1 | int Find(int p) { |
维护链信息
通过 split
和普通的 splay 树的左右子树合并即可维护和查询,此处不做过多展开。
维护连通
通过 find
即可维护动态加边删边的连通性问题。
维护边权
对每条边新建一个点即可,在 pushup
和 pushdown
的时候不考虑节点上的权值就好。
维护边双连通
只支持加边。
考虑加边的时候相当于把这条边所在的链缩成了一个点,用 LCT 跳跃,并查集合并即可。
维护子树信息
LCT 不能够较好地维护子树信息,但是只要有办法记录虚边连接的子树信息(例如子树节点数量等)就可以在 $O(n \log n)$ 的时间复杂度内维护。
(这个信息要支持撤销,即有可减性,否则无法维护)
(同时维护子树信息改的地方有点多,一定要注意细节)
时间复杂度
经过势能分析,时间复杂度为大常 $O(n \log n)$,如果把 splay 换成 Treap 的话,时间复杂度就是普通的 $O(n \log^2 n)$。
下面以 P3690 【模板】动态树(LCT) 为例,给出 LCT 的示例代码:
1 |
|
注意:一定要在合适的时候 pushdown
和 pushup
,不然很容易出错。