L C T \tt LCT LCT,即 Link \texttt{Link} Link- Cut \texttt{Cut} Cut- Tree \texttt{Tree} Tree,是一种维护森林的数据结构。
比树链剖分支持更多操作,比如说树上翻转权值。
前置知识
S p l a y \tt Splay Splay 和其文艺平衡树用法
算法用途
维护森林,以更大的常数和复杂度支持更多的操作。
算法复杂度
时间
O ( n log n + m log n ) O(n \log n + m \log n) O(nlogn+mlogn)
空间
O ( n ) O(n) O(n)
算法实现
实链剖分
实链剖分将一棵树分成实边和虚边,实链剖分后的一棵树满足如下要求:
- 树中一条边要么是实边要么是虚边。
- 树中每个节点只能连一条实边向其中一个儿子,连向其他儿子的都是虚边。
虚实是可以变化的,并不是固定的,在需要时可以将虚边改成实边,将实边改成虚边。
实链:相连的实边所组成的链
实儿子:这个节点在其所在的实链上的儿子。
LCT
L
C
T
\tt LCT
LCT 将森林中每棵树实链剖分一遍,虚边表示为父亲不认儿子,儿子认父亲,就是 father[v] = u, son[u] != v
,实边则是 father[v] = u, son[u] = v
(因为每个节点只有一个实边连向儿子,所以可以当成每个节点只指向一个孩子和一个父亲)。
然后,每条实链用一个 S p l a y \tt Splay Splay 来维护, S p l a y \tt Splay Splay 中序遍历时的节点顺序按照深度严格递增(可以理解为 S p l a y \tt Splay Splay 按照深度当作关键字)。为了方便,我们以后用一棵树来演示,而不是整个森林。
ACCESS
L C T \tt LCT LCT 最重要也是最难的操作是打通 a c c e s s ( x ) \tt access(x) access(x)
这个操作是为了打通一个从 x x x 到根节点的路径,使其变成一条实链。例如 a c c e s s ( L ) \tt access(L) access(L)
a
c
c
e
s
s
(
a
)
\tt access(a)
access(a) 的步骤如下(初始
x
=
a
,
y
=
0
\tt x = a, y = 0
x=a,y=0)
- s p l a y ( x ) \tt splay(x) splay(x),将 x \tt x x 伸展到其所在 s p l a y \tt splay splay 的根。
- 将 x \tt x x 的右儿子换成 y \tt y y,想想为什么是右儿子?因为 S p l a y \tt Splay Splay 的关键字是深度,所以 x 的右儿子就是原树中 x 的实儿子,将实儿子换成 y y y。
- y = x , x = f a t h e r [ x ] \tt y = x, x = father[x] y=x,x=father[x]
那么具体操作是什么呢?我们来看一下:
首先,将 L \tt L L 伸展至根
将
L
\tt L
L 的在
S
p
l
a
y
\tt Splay
Splay 中的右儿子换成
0
0
0,因为我们要打通
L
L
L 到
A
A
A,那么就需要将
L
\tt L
L 以下的那些节点割掉。
S
p
l
a
y
(
G
)
\tt Splay(G)
Splay(G),(既然没改变就直接与下一个操作合在一起讲了),将
G
G
G 右儿子换成
L
L
L,
直接将接下来两个展示了吧。
打通后的实链按照深度排序是 A C G J L \tt ACGJL ACGJL, S p l a y \tt Splay Splay 的中序遍历是 A C G J L \tt ACGJL ACGJL,没问题。
L C T \tt LCT LCT 最重要,最难的操作讲完了,后面就跟割草一样简单了。
MAKEROOT
m a k e r o o t ( y ) \tt makeroot(y) makeroot(y) 将 y \tt y y 变成其所在树的根节点。
我们假设 y \tt y y 为要换成的根, x \tt x x 为原来的根。
那么整棵树可以分成 4 4 4 个部分:
- x \tt x x 的儿子中不包含 y \tt y y 的那些子树的节点(红)
- y \tt y y 的子树(不包括 y \tt y y,蓝)
- x \tt x x 到 y \tt y y 的路径上的节点(包括 x , y \tt x,y x,y,紫和黑)
- 其他的节点(绿)
可见当根从
x
\tt x
x 转变到
y
\tt y
y 的时候,红色部分的父亲是不会改变的,蓝色和绿色也一样,只有紫色部分的会倒过来。
所以就可以这样更换根节点:
access(y), splay(y), reverse(y)
打通一条到原根节点的通道,伸展 y \tt y y,翻转 y \tt y y 子树中的节点
最后一步至关重要,我们想想: S p l a y \tt Splay Splay 中的节点按照深度排序,所以翻转后,这条 x x x 到 y y y 实链中的节点,就会深度颠倒,就实现了换根。
FINDROOT
f i n d r o o t ( x ) \tt findroot(x) findroot(x) 查找 x \tt x x 在原树中的根,多用于判断连通性。
先 access(x), splay(x)
然后一直找
x
\tt x
x 的左儿子,最左边的就是根了。
SPLIT
s p l i t ( x , y ) \tt split(x,y) split(x,y) 将 x \tt x x 到 y \tt y y 的路径装进一个 s p l a y \tt splay splay 里,这个很容易实现。
先将 x \tt x x 变成根,然后 a c c e s s ( y ) \tt access(y) access(y) 就行了。
LINK
l i n k ( x , y ) \tt link(x, y) link(x,y) 连一条 x , y \tt x, y x,y 之间的轻边,让 x x x 成为 y y y 的父亲。
makeroot(y), father[y] = x
CUT
c u t ( x , y ) \tt cut(x, y) cut(x,y),将 x , y \tt x,y x,y 之间的边断开。
断开简单啊:split(x,y), father[y] = c[x][0]=0;
如果不存在这条边呢?我们要如何判断这条边存不存在?
先 makeroot(x)
如果这条边存在,则一定满足下面的要求:
- f a t h e r [ y ] = x \tt father[y] = x father[y]=x
- f i n d r o o t ( y ) = x \tt findroot(y) = x findroot(y)=x
- c h i l d [ y ] [ 0 ] ! = 0 \tt child[y][0] != 0 child[y][0]!=0
如果全部满足,就代表这条边存在。就断开
L C T \tt LCT LCT 讲完了,不想放代码了。我学了 2 \tt 2 2 天才彻底理解 L C T \tt LCT LCT