LCT学习笔记
动态维护树上一些问题的数据结构,前置技能需要Splay
这个东西好像全世界都会了,就我没写过
本文大多数内容都是整理抄自网上各种博客和hyj的课件
基础知识
引入
Link-Cut-Tree
,简称LCT
学完树链剖分后,我们发现很多树上操作问题都可以用树剖解决。可是如果树的形态不断改变,即需要支持加边、删边操作,就需要用到LCT
了
实链剖分
类似于轻重链剖分,对于每个点将其与某个儿子的边划为实边,其余的划为虚边
实链剖分中虚实儿子会不断改变,因此我们用Splay
来维护,每条实链对应一个Splay
所以其实LCT
的本质就是利用树剖的思想,把整棵树拆成一条条实链,通过Splay
维护实链,从而动态维护树上信息
性质
- 每一个
Splay
维护的是一条从上到下按在原树中深度严格递增的路径, 其内部是以深度为关键字排序 - 每个节点包含且仅包含于一个
Splay
中 - 每个节点与它实边相连的点在同一个
Splay
中,实边包含在Splay
当中;虚边总是由一棵Splay
中的根连向另一棵Splay
中最深的节点。连向的节点记录父亲,连出的节点不记录儿子(认父不认子)
基本操作
作用:把到原树根节点的路径上打通,经过的边全部变成实边,使得原树根与在同一
Splay
中,且是这条路径的一个端点,以维护当前点到原树根的链上的信息19.3.29UPD
注意:一定是打通,实际上并没有旋到根的位置
实现:重复以下步骤直至与原树的根联通:
- 把点
splay
到当前Splay
的根 - 改变虚实儿子:把到父亲的实边变成虚边,把父亲连向自己的边变成实边
push_up
- 跳到父亲
- 把点
access = splay + change_child + push_up
作用:把变成原树的根
实现:
先
access(x)
,由于LCT
的性质,此时在当前Splay
中深度最大,只有左儿子翻转所在的整棵
Splay
,相当于翻转了到原树根的路径,使得当前点只有右儿子,变成深度最小,即为根具体实现时,先
splay(x)
,然后只需直接打翻转标记
19.5.29UPD
为什么要
splay()
?实际上
access(x)
时,x
是没有旋到根的,只是把路径打通。。。
make_root = access + splay + reverse
- 作用:找一个点所在原树的根
- 实现:
access(x)
并splay(x)
后,当前Splay
最左边的点即为原树的根,最后再splay
一下保证复杂度 - 注意:
find_root
的常数比较大,如果只有加边没有删边操作的话,可以用DSU
代替此操作- 在往左走的时候一定要记得
push_down
,且一开始就要push_down
一下
find_root = access + splay + go_left_child + splay
作用:将两个点之间的路径放到一个
Splay
中,获取这条链上的信息实现:
make_root(x)
使得成为原树的根,再access(y)
,最后splay(y)
最后
splay(y)
是用来更新信息的,同时保证Splay
的复杂度
split(x, y) = make_root(x) + access(y) + splay(y)
- 作用:连的边(这里默认把连到上)
- 实现:
make_root(x)
后,先判断find_root(y)
是否等于,如果是则说明已经相连,否则从 向连一条轻边。
link(x, y) = make_root(x) + (fa[x] = y)
作用:断开的边(这里默认把与父亲的边断掉)
实现:判断和之间是否真的存在一条边;若存在,直接切断
这里实现上有点复杂,解释一下这个代码吧
1
2
3
4
5
6make_root(x);
if (find_root(y) == x && Tree[y].fa == x && !ls(y))
{
Tree[y].fa = rs(x) = 0;
push_up(x);
}因为如果和在同一个联通块中,那么纵使在
find_root(y)
的开始把splay
到根,最后又把旋回到根了。所以在执行完find_root(y)==x
这条语句后当前Splay
的根仍然是
cut(x, y) = make_root(x) + (f[y] = right_child[x] = 0)
注意点
LCT
中的Splay
和普通Splay
的区别:- 在
rotate
时要判断if(!is_root(f))
,因为有可能已经为当前Splay
的根,如果不判断的话就会把它向父亲的虚边变实,而这并不是我们所期望的 - 在
splay
中循环条件由while(Tree[x].fa != y)
变成了while(!is_root(x)
splay
中需要先从根push_down
到当前点,再rotate
- 在
push_down
一般写法与线段树的有点区别:当前点如果有翻转标记的话,它的左右儿子是还没有被翻转的,在标记下传的同时进行翻转。因此在find_root
往左儿子走之前就要push_down
一下。其实也就是每访问一个点之前就要push_down
!UPD 19.3.31
LCT
中make_root
是一个重要的操作,除了access
之外的所有操作都建立在make_root
之上
至此,LCT
的基本操作就学习口胡完啦
我是不会告诉你我到现在还没有开始码的
应用
模拟
用来模拟题目中类似的过程,往往在里面再套上一个数据结构来维护一些信息
维护联通性
直接按照题目link
与cut
查询两点连通性:find_root
,如果没有cut
的话可以直接用DSU
维护链上信息
要修改原树中到的路径上的信息
直接split(u,v)
,然后在Splay
中操作,利用push_down
的lazy标记完成所有点的改变
维护生成树
维护一个图的最小或最大生成树
因为LCT
无法维护边权,所以考虑化边为点。原图中一条边的连接,在新图中变为两条边,即:
原点——新点——原点
原点不带权,新点带权,其权值为原来的边权
LCT
维护一下链上最大值和次大值,每次直接连边,或是split
更新答案
维护边双
加边的时候,如果这两个点已经联通,那么这条边加入后就一定会形成一个环,因此这个环里的所有点就处在同一个边双里
用类似缩点的方法,遇到环,则新建一个点代表这个边双,把这个环里的所有点的父亲指向新点
凡要用到一个点就x = fa[x]
,相当于踏入这个环就踏进这个新点
这个操作用DSU
实现就好
维护子树信息(只能维护,无法修改)
我们发现,在LCT
中都是维护的链上信息,那么如何维护原树子树信息呢?
不难发现,原树子树信息 = 虚子树 + 实子树 + 自身信息
于是相当于需要多维护一个虚子树信息
对于每个父亲与自己不在同一Splay
中的点,把自己的信息加入到自己存的父亲的虚儿子信息库
如果维护信息是可减的,一个数组存,更新是
如果维护信息不可减(如min,max),就需要堆或者其它数据结构维护,复杂度多一个
相对于原来的LCT
,发生改变的操作有push_up
,access
,link
变化的原因:
push_up
:要多维护虚子树的信息access
:在改变虚边实边时,的虚子树信息会变化,所以要修改link
:在把向连虚边的时候,的虚子树信息会变化,同时因为每个点的子树信息要算上虚子树信息,所以所有祖先的子树信息都会变化,需要暴力把所有祖先修改,很麻烦。但是我们可以先直接make_root(y)
,这样就没有祖先了
可以发现,需要修改的地方都是虚边改变了的地方,根本原因是push_up
时只维护了总子树信息,并没有维护虚儿子信息。所以需要手动修改
至此,LCT
的基本功能又学习口胡完啦
我是不会告诉你我到现在还是没有开始码的
模板(Luogu P3690)
1 |
|