LCT维护一个森林,即把每个节点用splay维护,可以进行许多操作:
查询、修改链上的信息
随意指定原树的根(即换根)
动态连边、删边
合并两棵树、分离一棵树
动态维护连通性
等
主要性质
- 每一个Splay维护的是一条从上到下按在原树中深度严格递增的路径,且中序遍历Splay得到的每个点的深度序列严格递增。
- 每个节点仅包含于一个splay中。
- 边分为实边和虚边,实边记录
son
和fa
,包含在一个 splay 中。为了维护 splay 树形,虚边仅记录fa
。不过虚边是由 splay(根) 指向父亲的,不一定是原节点。
操作
access
access
操作是指将一个点到树根的路径打通,即把根节点和该节点搞到一个 splay
上。
我们从 x 向上爬。
- 每次将所在节点 splay(转到 splay 的根节点)
- 将该 splay 所指向的节点的儿子换为 splay 的根节点。
- 更新信息。
- 将操作点切换到父节点,重复操作直到节点的父亲是0。
1 | void access(int x){ |
makert
makert
操作可以将一个节点变成整棵树的根。
- 将该节点
access
。 - 将该节点
splay
。 - 将该节点打上子树翻转标记。
正确性为,access
操作后将该节点到原来的根的路径打通并成为一个 splay
后,整条路径的 dfs 序都会反转,而其他节点的 dfs 序都不会变。
1 | inline void rev(const int &x){tag[x]^=1,swap(son[x][0],son[x][1]);} |
findrt
findrt
操作可以找到一个节点在其树内的根。
- 将该节点
access
。 - 将该节点
splay
。 - 一直跳左儿子,则找到 dfs 序最小的节点,也就是根。
1 | int findrt(int x){access(x),splay(x);while(son[x][0])x=son[x][0];splay(x);return x;} |
注意,上面的代码中如果不在找到根后 splay
复杂度是假的。
link
link
操作将两个连通块进行连边。
- 若要在连边之前判断两者是否已经联通,可以将一个节点变成根,查找另一个节点的根进行判断。
- 一般连边是将一个节点变成另一个节点的虚儿子,也就是连虚边。这种方式适用于虚儿子贡献较为简单计算的情况。设这两个节点为
x 和 y,我们将 y
makert
,将 xsplay
,然后将 y 的fa
改成 x 即可。(如果要统计子树信息的话,将两个节点都改为根,然后连边时顺便统计字数贡献) - 当然也可以直接连成实边。
1 | void link(int x,int y){ |
1 | inline void link(int x,int y){ |
cut
cut
操作将两个点间进行删边。
- 若要判断两个点原先是否有边相连,先将一个节点设成根然后判断连通性,再判断两点间的 dfs 序是否连续。
- 然后直接将上面节点的儿子和下面节点的父亲设为 0 即可。别忘了更新信息。
1 | inline void cut(int x,int y){ |
模板
维护链上最大值。
1 | struct LCT{ |
进阶
维护子树信息
LCT 可以维护子树信息,但是只能做到查询而做不到修改。简单来说,维护的方式就是每次给一个 splay 添加一个虚儿子的时候,需要多开一个数据结构记录虚儿子的贡献。然后在上传的时候考虑虚儿子即可。
1 |
|
动态求LCA
LCT 本来就是动态的,如何求两个点的 LCA 呢?
将其中一个点 access
,然后将另外一个点
access
,并记录最后一次 splay
前找到的节点(即最后的代码中的y)
利用LCT的结构
LCT
是一种优秀的暴力,它的结构有时候可以帮我们做一些很强的题目(虽然一般都想不到这个模型)
思路:观察操作,有“将一个点到根节点的路径染成同一种新的颜色”,发现同一颜色的连通块都是一条链,那么我们很快想到
LCT 的模型。维护的答案是该节点到根的 splay 个数。那么我们在改变
access
的时候,即改变儿子虚实的时候,需要将虚儿子子树内所有节点的答案都增加,将实儿子子树内所有节点都减少,这个可以用线段树进行维护。
1 |
|
P6292 区间本质不同子串个数也用到了这个 trick。
更多trick
待耕。