概述:
LCT是用于维护动态森林的数据结构. 可以做到$O(\log n)$的查询, 切割, 连接操作.
LCT基于树链剖分的思想, 即将一棵树剖成多条树链, 方便采用数据结构处理.
具体来说, LCT采用了”实链剖分“, 即每个点指定一个”实儿子”, 通过”实儿子”连成”实链”. 与普通的树链剖分区别在于”实儿子”是动态变化的.
LCT使用splay来维护树链. 由于一条树链上的节点点深度各不相同. 所以可以使用深度作为关键字建立splay. 每个节点的左子树深度比该节点小, 右子树深度比该节点大.
树链之间的连接关系可以在splay的树根上做文章: 将splay的根的父节点指向这个splay对应树链深度最小的点在原树上的父节点. 这样整棵树便连接起来了, 同时区分出了树上的虚实关系: 实边双向连接, 虚边单向连接.
操作:
需要实现LCT首先需要实现splay, 以luoguP3690为例, 代码与节点定义如下所示:
1 | struct td { |
access(x)
将根到x的所有边变为实边, 这条路径上节点的其余边全部变为虚边. 此时x为根所在的实链中最深的节点.
本质上就是将x接入根所在的splay中. 由于LCT的特性, 需要从下往上处理.
首先使用splay的操作将x伸缩到其所在splay的根. 此时其splay右子树为深度比x大的实链节点, 需要将其断开.
同时由于x在实链splay的根上, 其父指针指向了这个splay对应实链深度最小的点在原树上的父节点. 将该节点的右儿子设为x, 就使得x所在实链与上面的实链连接起来了.
重复这个步骤直到x的父指针为空, access操作就完成了.
1 | void acs(int x) { |
makeroot(x)
将x变为原树的根.
这个操作的作用主要是进行原树上的路径操作, 将路径的其中一个点变为根后对另一个点access即可在一个splay中进行原树上的路径操作.
将x access后将x和根所在的splay翻转即可. 可以脑补一下将x到原根这条链的深度翻转的结果.
1 | void mkr(int x) { |
findroot(x)
寻找x所在的原树的根.
这个操作可以用来判断两个节点是否在同一个树中, 也即将LCT视作一个支持删边的”并查集”.
将x access后寻找x所在splay中深度最小的节点即可.
1 | int fdr(int x) { |
link(x,y)
连接x和y.
makeroot y后access并splay x然后再将y的父节点设为x(即x与y连接一条虚边).
将x access并splay是为了维护节点x.
注意先判断x与y是否在同一棵树上.
1 | void lnk(int x,int y) { |
cut(x,y)
断开x与y的边.
makeroot x后首先使用findroot y判断x与y是否在同一棵树上, 若x与y有边连接则y为x的右儿子且y没有左儿子. 此时断开x与y的连接即可.
1 | void cut(int x,int y) { |
一些技巧:
边权变点权:
一些题目需要维护的是边权而不是点权, 此时可以将原树上的边也看作点, 点权即为原树上的边权. 加边删边时将对应边的点两边连边删边即可.
由于森林的边数最多为n-1条, 所以空间增加了一倍.
维护子树信息:
维护子树信息需要每个节点同时维护实子树与虚子树的信息, 而LCT”认父不认子”的特性使得虚子树的信息不太方便维护.
解决的方法是对每个点统计其所有虚子树的贡献, 并且在涉及改变splay形态的操作中即时修改.
access操作中每次循环中当前splay的根节点的原右儿子与新右儿子的虚实发生了变化, 此时需要减去新右儿子的贡献并加上原右儿子的贡献.
link操作连接了一条虚边, 所以此时需要加上新虚儿子的贡献.
可以看出使用LCT维护的子树信息需要满足可减性, 例如子树权值和, 反例是子树最大值. 否则在access操作中无法维护虚子树信息.