Link Cut Tree(动态树)(坑)

前置知识

  1. Splay基本操作
  2. 树链剖分
  3. Splay维护区间信息

概念

链剖分

链剖分,是指一类对树的边进行轻重划分的操作,这样做的目的是为了整体地维护树上的连续一段节点/边,以优化时间复杂度。链剖分一般分为三种,重链剖分,实链剖分和并不常见的长链剖分。

重链剖分

重链剖分其实就是我们所说的树剖
定义每个点的size域为它的儿子的个数,则定义某个点的重儿子为它的所有儿子中,size域最大的那个儿子。则连接它的它重儿子的边为重边,而连接其他儿子的边称为轻边
若干重边连接在一起构成重链,因为重链上的节点编号连续,因此可以用某些数据结构维护重链内部的关系。

实链剖分

与重链剖分不同地,实链剖分并不按照size域来决定那一条边是重链,而是可以变化的,因此一般使用更加灵活的Splay来维护(据说还可以用Treap维护),而LCT就是以实链剖分为基础的。

长链剖分

长链剖分在信息学竞赛中并不常见。它定义所有节点的深度为其到根的距离,则某个节点的重儿子为它的所有子树中,含有最大深度的节点的子树,其余定义和重链剖分基本一致。

LCT

在实链剖分的基础上,LCT支持很多重链剖分做不到的操作
- 查询、修改树上信息
- 换根
- 动态连边、删边
- 合并、分离不同的树
- 维护联通性(适用范围比并查集更广)

主要性质/定义

  • 每个Splay维护的是一条在原树中深度严格递增的路径,且以深度为关键字排序。

    图中的每个方框表示一棵Splay,由于Splay的性质,旋转不会破坏中序遍历,因此不论怎么旋转都可以保证树的形态唯一。
  • 每个节点包含且仅包含于一个Splay中
  • 树链剖分中,虽然说是把每条链交给一个线段树管理,但更多时候却是重新编号,在保证重链的编号连续的前提下,用一大棵线段树来管理。
    类似地,LCT中,所有的Splay其实是一棵Splay,其中的实线边为真正的Splay的边,最多只有两条(即左右儿子),而且关系是双向的,而虚边则仅仅由一个一棵Splay指向另一棵Splay中的某个点,更准确来说,应该指向另一棵Splay的,关系是单向的,也就是说,父亲是不记录它的编号的。
    为什么这里可以直接指向根呢?因为根据Splay的性质,可以很快地找到真正指向的元素—,即Splay中的最小元素。

操作

access(x) a c c e s s ( x )

作用:把 x x 到树根的唯一路径上的所有边变为实边,且让 x x 成为所在的重链中的深度最大的结点。
下面以 access(N) a c c e s s ( N ) 为例子。

具体怎么实现呢?根据性质一,旋转不会改变原树的结构,因此可以先把 N N 提到它所在的Spaly的根,即 Splay(N) S p l a y ( N )

因为要保证 access(N) a c c e s s ( N ) 之后, x x 是其所在的重链中深度最深的结点,因此不可能有结点的深度比 N N 还大,所以必须把 N N 的右子树独立开成另外一棵Splay。
因为虚边是单向的(父节点不认子节点),因此在实现上直接让 N N 的右儿子为空就好啦。

对应到原图即为:

接着 Splay(I=N>father) S p l a y ( I = N − > f a t h e r )

然后关键部分来了。既然我们要让 N N 到树根的唯一路径上的所有边变为实边,那么 I>N I − > N 自然也不例外。因此,我们要让 I>N I − > N 的边成为实边,那就让 N N 取缔 I I 右儿子,为什么一定是右儿子呢?
回顾一下性质1:

每个Splay维护的是一条在原树中深度严格递增的路径,且以深度为关键字排序。

也就是说,在同一棵Splay中必须按照深度递增,而N是I的儿子,因此N的深度比较N更大,所以必须取缔右儿子的位置。

对应到原图:

然后再 Splay(H=i>father) S p l a y ( H = i − > f a t h e r )

同理, I I 取缔 H H 右儿子

对应到原图

Splay(A=G>father) S p l a y ( A = G − > f a t h e r )

H H 取缔 A A 儿子。

对应到原图:

大功告成!

rooted(x) r o o t e d ( x )

作用:让 x x 成为树根。
很多时候,仅仅让一个点和根节点在同一棵Splay中并不能满足要求,因为可能遇到某些节点的深度是相同的,根据性质一,这两个点不可能再同一棵Splay中,这种时候,就必须换根。
下面以 rooted(N) r o o t e d ( N ) 为例子

access(N) a c c e s s ( N )

然后 Splay(N) S p l a y ( N )

这里不论是单旋还是双旋都没有问题,不会影响程序的正确性(因为不会影响到根节点和其他节点的大小),但有可能对时间有一定影响。
然后就是重头戏啦!把 N N 所在的整棵 Splay 对称翻转。即让深度小的变为深度大的,深度大的变为深度小的。
但是这个操作的时间复杂度似乎是 O(n) O ( n ) 的,那有没有什么好方法呢?回忆一下曾经的Splay模板之一:文艺平衡树
因此可以打上旋转标记。

在原图也就是把整条链从 N N 拎起来。

完成!

split(x,y) s p l i t ( x , y )

作用:让 x x y y 在同一棵Splay中。
有了access和rooted函数,就可以方便地拉出一条链了。

模板

题表

你可能感兴趣的:(LCT)