Link Cut Tree
简介
Link/Cut Tree 是一种数据结构,我们用它来解决 动态树问题。
Link/Cut Tree 又称 Link-Cut Tree,简称 LCT,但它不叫动态树,动态树是指一类问题。
Splay Tree 是 LCT 的基础,但是 LCT 用的 Splay Tree 和普通的 Splay 在细节处不太一样(进行了一些扩展)。
问题引入
维护一棵树,支持如下操作:
- 修改两点间路径权值。
- 查询两点间路径权值和。
- 修改某点子树权值。
- 查询某点子树权值和。
这是一道树剖模版题。
但是再加一个操作:
- 断开并连接一些边,保证仍是一棵树。
要求在线求出上面的答案。
这就成了动态树问题,可以使用 LCT 求解。
动态树问题
维护一个 森林,支持删除某条边,加入某条边,并保证加边,删边之后仍是森林。我们要维护这个森林的一些信息。
一般的操作有两点连通性,两点路径权值和,连接两点和切断某条边、修改信息等。
从 LCT 的角度回顾一下树链剖分
- 对整棵树按子树大小进行剖分,并重新标号。
- 我们发现重新标号之后,在树上形成了一些以链为单位的连续区间,并且可以用线段树进行区间操作。
转向动态树问题
我们发现我们刚刚讲的树剖是以子树大小作为划分条件。那我们能不能重定义一种剖分,使它更适应我们的动态树问题呢?
考虑动态树问题需要什么链。
由于动态维护一个森林,显然我们希望这个链是我们指定的链,以便利用来求解。
实链剖分
对于一个点连向它所有儿子的边,我们自己选择一条边进行剖分,我们称被选择的边为实边,其他边则为虚边。对于实边,我们称它所连接的儿子为实儿子。对于一条由实边组成的链,我们同样称之为实链。请记住我们选择实链剖分的最重要的原因:它是我们选择的,灵活且可变。正是它的这种灵活可变性,我们采用 Splay Tree 来维护这些实链。
LCT
我们可以简单的把 LCT 理解成用一些 Splay 来维护动态的树链剖分,以期实现动态树上的区间操作。对于每条实链,我们建一个 Splay 来维护整个链区间的信息。
辅助树
我们先来看一看辅助树的一些性质,再通过一张图实际了解一下辅助树的具体结构。
在本文里,你可以认为一些 Splay 构成了一个辅助树,每棵辅助树维护的是一棵树,一些辅助树构成了 LCT,其维护的是整个森林。
- 辅助树由多棵 Splay 组成,每棵 Splay 维护原树中的一条路径,且中序遍历这棵 Splay 得到的点序列,从前到后对应原树「从上到下」的一条路径。
- 原树每个节点与辅助树的 Splay 节点一一对应。
- 辅助树的各棵 Splay 之间并不是独立的。每棵 Splay 的根节点的父亲节点本应是空,但在 LCT 中每棵 Splay 的根节点的父亲节点指向原树中 这条链 的父亲节点(即链最顶端的点的父亲节点)。这类父亲链接与通常 Splay 的父亲链接区别在于儿子认父亲,而父亲不认儿子,对应原树的一条 虚边。因此,每个连通块恰好有一个点的父亲节点为空。
- 由于辅助树的以上性质,我们维护任何操作都不需要维护原树,辅助树可以在任何情况下拿出一个唯一的原树,我们只需要维护辅助树即可。
现在我们有一棵原树,如图所示。(加粗边是实边,虚线边是虚边。)
由刚刚的定义,辅助树的结构如图所示。
考虑原树和辅助树的结构关系
- 原树中的实链 : 在辅助树中节点都在一棵 Splay 中。
- 原树中的虚链 : 在辅助树中,子节点所在 Splay 的 Father 指向父节点,但是父节点的两个儿子都不指向子节点。
- 注意:原树的根不等于辅助树的根。
- 原树的 Father 指向不等于辅助树的 Father 指向。
- 辅助树是可以在满足辅助树、Splay 的性质下任意换根的。
- 虚实链变换可以轻松在辅助树上完成,这也就是实现了动态维护树链剖分。
接下来要用到的变量声明
ch[N][2]
左右儿子f[N]
父亲指向sum[N]
路径权值和val[N]
点权tag[N]
翻转标记laz[N]
权值标记siz[N]
辅助树上子树大小- Other_Vars
函数声明
一般数据结构函数(字面意思)
PushUp(x)
PushDown(x)
Splay 树的函数
下面是 Splay 树中用到的函数,具体可以查阅 Splay 树。
Get(x)
获取 是父亲的哪个儿子。Splay(x)
通过和 Rotate 操作联动实现把 旋转到 当前 Splay 的根。Rotate(x)
将 向上旋转一层的操作。
新操作
Access(x)
把从根到 的所有点放在一条实链里,使根到 成为一条实路径,并且在同一棵 Splay 里。只有此操作是必须实现的,其他操作视题目而实现。IsRoot(x)
判断 是否是所在树的根。Update(x)
在Access
操作之后,递归地从上到下PushDown
更新信息。MakeRoot(x)
使 点成为其所在树的根。Link(x, y)
在 两点间连一条边。Cut(x, y)
把 两点间边删掉。Find(x)
找到 所在树的根节点编号。Fix(x, v)
修改 的点权为 。Split(x, y)
提取出 间的路径,方便做区间操作。
宏定义
#define ls ch[p][0]
#define rs ch[p][1]
函数讲解
PushUp()
1 2 3 4 |
|
PushDown()
1 2 3 4 5 6 |
|
Splay() && Rotate()
这里 Splay()
和 Rotate()
与 Splay 树的实现有些区别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
以上函数可以查阅 Splay 树。
下面是 LCT 独有的函数。
isRoot()
1 2 3 |
|
Access()
1 2 3 4 5 6 7 8 9 10 |
|
-
我们有这样一棵树,实线为实边,虚线为虚边。
-
它的辅助树可能长成这样(构图方式不同可能 LCT 的结构也不同)。
-
现在我们要
Access(N)
,把 到 路径上的边都变为实边,拉成一棵 Splay。 -
实现的方法是从下到上逐步更新 Splay。
-
首先我们要把
旋至当前 Splay 的根。 -
为了保证 AuxTree(辅助树)的性质,原来
到 的实边要更改为虚边。 -
由于认父不认子的性质,我们可以单方面的把
的儿子改为NULL
。 -
于是原来的 AuxTree 就从下图变成了下下图。
-
下一步,我们把
指向的 Father 也旋转到 的 Splay 树根。 -
原来的实边
— 要去掉,这时候我们把 的右儿子指向 ,就得到了 — 这样一棵 Splay。 -
接下来,按照刚刚的操作步骤,由于
的 Father 指向 ,我们把 旋转到他所在 Splay Tree 的根,然后把 的 rs 设为 。 -
之后的树是这样的。
-
同理我们
Splay(A)
,并把 的右儿子指向 。 -
于是我们得到了这样一棵 AuxTree。并且发现
— 的整个路径已经在同一棵 Splay 中了。
1 2 3 4 5 6 7 8 |
|
我们发现 Access()
其实很容易,只有如下四步操作:
- 把当前节点转到根。
- 把儿子换成之前的节点。
- 更新当前点的信息。
- 把当前点换成当前点的父亲,继续操作。
这里提供的 Access 还有一个返回值。这个返回值相当于最后一次虚实链变换时虚边父亲节点的编号。该值有两个含义:
- 连续两次 Access 操作时,第二次 Access 操作的返回值等于这两个节点的 LCA.
- 表示
到根的链所在的 Splay 树的根。这个节点一定已经被旋转到了根节点,且父亲一定为空。
Update()
1 2 3 4 5 |
|
makeRoot()
Make_Root()
的重要性丝毫不亚于Access()
。我们在需要维护路径信息的时候,一定会出现路径深度无法严格递增的情况,根据 AuxTree 的性质,这种路径是不能出现在一棵 Splay 中的。- 这时候我们需要用到
Make_Root()
。 Make_Root()
的作用是使指定的点成为原树的根,考虑如何实现这种操作。- 设
Access(x)
的返回值为 ,则此时 到当前根的路径恰好构成一个 Splay,且该 Splay 的根为 . - 考虑将树用有向图表示出来,给每条边定一个方向,表示从儿子到父亲的方向。容易发现换根相当于将
到根的路径的所有边反向(请仔细思考)。 - 因此将
到当前根的路径翻转即可。 - 由于
是 到当前根的路径所代表的 Splay 的根,因此将以 为根的 Splay 树进行区间翻转即可。
1 2 3 4 5 |
|
Link()
- Link 两个点其实很简单,先
Make_Root(x)
,然后把 的父亲指向 即可。显然,这个操作肯定不能发生在同一棵树内,所以记得先判一下。
1 2 3 4 5 |
|
Split()
Split
操作意义很简单,就是拿出一棵 Splay,维护的是 到 的路径。- 先
MakeRoot(x)
,然后Access(y)
。如果要 做根,再Splay(y)
。 - 另外 Split 这三个操作可以直接把需要的路径拿出到
的子树上,可以进行其他操作。
Cut()
Cut
有两种情况,保证合法和不一定保证合法。- 如果保证合法,直接
Split(x, y)
,这时候 是根, 一定是它的儿子,双向断开即可。就像这样:
1 |
|
如果是不保证合法,我们需要判断一下是否有,这里选择使用 map
存一下,但是这里有一个利用性质的方法:
想要删边,必须要满足如下三个条件:
连通。 的路径上没有其他的链。 没有右儿子。
总结一下,上面三句话的意思就一个:
具体实现就留作一个思考题给大家。判断连通需要用到后面的 Find
,其他两点稍作思考分析一下结构就知道该怎么判断了。
Find()
Find()
查找的是 所在的 原树 的根,请不要把原树根和辅助树根弄混。在Access(p)
后,再Splay(p)
。这样根就是树里深度最小的那个,一直往左儿子走,沿途PushDown
即可。- 一直走到没有 ls,非常简单。
- 注意,每次查询之后需要把查询到的答案对应的结点
Splay
上去以保证复杂度。
1 2 3 4 5 6 7 8 |
|
注意事项
- 操作前一定要想一想需不需要
PushUp
或者PushDown
,LCT 由于特别灵活的原因,少Pushdown
或者Pushup
一次就可能把修改改到不该改的点上! - LCT 的
Rotate
和 Splay 的不太一样,if (z)
一定要放在前面。 - LCT 的
Splay
操作就是旋转到根,没有旋转到谁儿子的操作,因为不需要。
时间复杂度
LCT 中的大部分操作都基于 Access
,其余操作的时间复杂度都为常数,因此我们只需要分析 Access
操作的时间复杂度。
其中,Access
的时间复杂度主要来自于多次 splay 操作和对路径中虚边的访问,接下来分别分析这两部分的时间复杂度。
-
splay
-
定义
,其中 表示以 为根的所有虚边和实边的数量之和。 -
定义势能函数
,其中 表示所有节点的集合。
-
由 Splay 的时间复杂度 分析易知,splay 操作的均摊时间复杂度为
-
访问虚边
参考 重链剖分,定义两种虚边:
-
重虚边:从节点
到其父节点的虚边,其中 。 -
轻虚边:从节点
到其父节点的虚边,其中 。
对于虚边的处理,可以使用势能分析,定义势能函数
为所有重虚边的数量,定义均摊成本 ,其中 为实际操作的成本, 为势能的变化。-
走过重虚边后,会将重虚边转换为实边,该操作会减少
的势能,因为它通过加强重要连接来优化树的结构。且由于其实际操作成本为 ,抵消了势能的增加,故不会增加均摊成本,所有的均摊成本集中在轻虚边的处理上。 -
每次
Access
操作最多遍历 条轻虚边,因此至多消耗 的实际操作成本,转化得到 条重虚边,即势能以 的代价增加。
由此,最终访问虚边的均摊复杂度为实际操作成本和势能变化的和,即
。 -
综上所述,LCT 中 Access
操作的时间复杂度是 splay 和 虚边访问的复杂度之和,因此最后的均摊复杂度为 Access
操作的时间复杂度为 Access
操作的 Cut
,Link
,Findroot
等操作的均摊复杂度也为
习题
维护树链信息
LCT 通过 Split(x,y)
操作,可以将树上从点
例题 「国家集训队」Tree II
给出一棵有
- u1 v1 u2 v2
:将树上 两点之间的边删除,连接 两点,保证操作合法且连边后仍是一棵树。+ u v c
:将树上 两点之间的路径上的点权都增加 。* u v c
:将树上 两点之间的路径上的点权都乘以 。-
/ u v
:输出树上 两点之间的路径上的点权之和对 取模后的值。-
操作可以直接Cut(u1,v1),Link(u2,v2)
。
对树上 Split(u,v)
。
此题要求进行在辅助树上的子树加,子树乘,子树求和操作,所以我们除了一般 LCT 需要维护的子树翻转标记,还要维护子树加法标记和子树乘法标记。处理标记的方法和在 Splay 上是一样的。
在打上和下传加法标记时,子树权值和的变化量和子树中的结点数有关,所以我们还要维护子树的大小 siz
。
在下传标记时,需要注意顺序,先下传乘法标记再下传加法标记。子树翻转和子树加乘两种标记没有冲突。
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
|
习题
维护连通性质
判断是否连通
借助 LCT 的 Find()
函数,可以判断动态森林上的两点是否连通。如果有 Find(x)==Find(y)
,则说明
例题 「SDOI2008」洞穴勘测
一开始有
Connect u v
:在 两点之间连接一条边。Destroy u v
:删除在 两点之间的边,保证之前存在这样的一条边。Query u v
:询问 两点是否连通。
保证在任何时刻图的形态都是一个森林。
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
|
维护边双连通分量
如果要求将边双连通分量缩成点,每次添加一条边,所连接的树上的两点如果相互连通,那么这条路径上的所有点都会被缩成一个点。
例题 「AHOI2005」航线规划
给出
0 u v
:删除 之间的连边,保证此时存在这样的一条边。1 u v
:查询此时 两点之间可能的所有路径必须经过的边的数量。
保证图在任意时刻都连通。
$1
本页面最近更新:2024/12/4 12:44:49,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:hsfzLZH1, Ir1d, sshwy, Tiphereth-A, Early0v0, Marcythm, ChungZH, i-yyi, mao1t, ouuan, Xeonacid, abc1763613206, Chrogeek, Henry-ZHR, StudyingFather, 9lie, alphagocc, Backl1ght, CCXXXI, CoderOJ, diauweb, Enter-tainer, GavinZhengOI, HeRaNO, ksyx, lirunzhe, Macesuted, mcendu, Molmin, Mout-sea, r-value, Sheng-Horizon, shuzhouliu, thredreams, vincent-163, yuhuoji, yyyu-star
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用