Link-Cut Tree 是一种用来维护动态森林连通性的数据结构,适用于动态树问题。它采用类似树链剖分的轻重边路径剖分,把树边分为实边和虚边,并用 Splay 来维护每一条实路径。Link-Cut Tree 的基本操作复杂度为均摊 ,但常数因子较大,一般效率会低于树链剖分。
定义
对于原森林,引入如下定义:
- 偏爱儿子(Preferred Child),一个节点最多有一个偏爱儿子;
- 偏爱边(Preferred Edge),偏爱儿子与其父亲相连的边。
- 偏爱路径(Preferred Path),由偏爱边连接而成的链;
原森林中偏爱路径由非偏爱边连接,构成 Link-Cut Tree。我们称 Link-Cut Tree 上的非偏爱边为虚边。同样的,对于以外的边称之为实边。
实边和虚边都是有向的,由子节点指向父节点。首尾相连的实边组成的不可延伸的链叫做路径。路径中深度最大的节点称为路径头部,深度最小的节点称为路径尾部。
对于 Link-Cut Tree 上的一条路径,用 Splay 树来维护信息,以节点的深度为关键字排序。Splay 树中一个节点的左子树所有节点比当前节点在森林中的深度小,右子树的所有节点比当前节点在森林中的深度大。不难发现,同一条路径上的点深度连续且互不相同。
实现时只需维护 Link-Cut Tree。其中每一棵 Splay 树的形态影响 Link-Cut Tree 的形态,但由于 Splay 树上的节点按照深度有序排列着(不改变相对次序),故对用 Link-Cut Tree 处理原森林节点关系没有影响。
在记录 Link-Cut Tree 时,用 fa
存储父亲的信息。如果当前点是其所在 Splay 树的根节点,其 fa
指向所在的路径的路经尾部在原森林中的父亲,否则指向其在 Splay 树的父亲节点。但是每个节点的儿子信息则完全是 Splay 树中的儿子信息,即轻边的儿子不会在父节点有记录。因此我们只需更改每个节点的右儿子即可,并不需要重置右儿子的父亲信息。
struct Node {
int fa, ch[2];
int sum, max, value;
bool reversed;
} Tr[MAXN];
操作
Link-Cut Tree 支持以下几种基本操作:
Access(u)
,“访问”某个节点 u u u,被“访问”过的节点会与根节点之间以偏爱路径相连,并且该节点为路径头部(最下端);MakeRoot(u)
,将某个节点 u u u 置为其所在树的根节点;FindRoot(u)
,查找某个节点 u u u 所在树的根节点;Link(u, v)
,将两个节点 u u u 和 v v v 连接,执行操作后 u u u 成为 v v v 的父节点;Cut(u, v)
,将两个节点 u u u 和 v v v 分离,执行操作后 v v v 及其子节点组成单独的一棵树;
操作表面上是对原森林处理,本质上是在 Link-Cut Tree 上做文章。
Access
操作
Access
操作是动态树问题最重要的操作,其目的是在同一棵 Splay 树上进行查询,修改,删除操作,且保证这棵树上只有需要查询的一条链上的点。
Access(u)
将根节点到点
u
u
u 的路径变为偏爱路径,将点
u
u
u 与其儿子的连边均变为非偏爱边。
对于当前点到根路径上的每一个点,不外乎进行如下操作:
-
将当前点 u u u 与儿子的连边均变为非偏爱边。
将点 u u u 旋转至当前 Splay 树的根,令其右儿子为 null \textrm{null} null。
-
设点 u u u 位于路径上的儿子为 v v v,使点 u , v u,v u,v 的连边成为偏爱边。
在操作 1 的基础上,将点 u u u 的右儿子指向点 v v v 所在的 Splay 树。此时 u → f a u\rightarrow fa u→fa 是轻边。
void access(int u) {
for(int v = 0; u; v = u, u = Tr[u].fa)
splay(u), Tr[u].ch[1] = v, pushup(u);
}
MakeRoot
操作
MakeRoot(u)
将
u
u
u 置为其所在树的根节点。该操作等价于把点
u
u
u 到根节点所经过的所有边方向取反。
首先执行 Access(u)
,将该节点与根节点之间用一条完整的路径连接,然后翻转这条路径即可。
void makeRoot(int u) {
access(u), splay(u), Tr[u].reversed ^= 1;
}
FindRoot
操作
FindRoot(u)
查找某个节点
u
u
u 所在树的根节点,主要用以判断连通性。
首先执行 Access(u)
,将该节点与根节点之间用一条完整的路径连接。由于根节点深度最小,直接在 Splay 树上寻找。
int findRoot(int u) {
access(u), splay(u);
pushdown(u); while(Tr[u].ch[0]) pushdown(u = Tr[u].ch[0]);
return u;
}
Link
操作
Link(u, v)
将两个节点
u
u
u 和
v
v
v 连接,执行操作后
u
u
u 成为
v
v
v 的父节点。
首先执行 MakeRoot(v)
,然后将
v
v
v 的父亲指向
u
u
u,相当于连一条轻边。
void link(int u, int v) {
makeRoot(v);
if (findRoot(u) != v) Tr[v].fa = u; //判断连边的合法性
}
Cut
操作
Cut(u, v)
将两个节点
u
u
u 和
v
v
v 分离,执行操作后
v
v
v 及其子节点组成单独的一棵树。
依次进行如下操作:
- 将点 u u u 置为其所在树的根节点,以保证 v v v 是 u u u 的子节点;
- 对
v
v
v 执行
Access
操作,将 v v v 与 u u u 之间用一条完整的路径连接; - 对
v
v
v 执行
Splay
操作,将 v v v 置于其所在 Splay 树的根节点; - 在 Splay 树上将 v v v 与其左子树分离,即将路径断开。
void cut(int u, int v){
makeRoot(u), access(v), splay(v);
if (tr[v].ch[0] != u || tr[tr[v].ch[0]].ch[1]) return; //为保证合法,要求二者深度连续。
Tr[u].fa = Tr[v].ch[0] = 0, pushup(v);
}
实现
IsRoot
函数
IsRoot(u)
函数判断点
u
u
u 是否为一棵 Splay 树的根节点。如果是,则返回真值。
根据对 fa
的定义,不难得到判断方法。
bool isRoot(int x) {
return Tr[Tr[x].fa].ch[0] != x && Tr[Tr[x].fa].ch[1] != x;
}
Splay 操作
bool get(int x) {
return Tr[Tr[x].fa].ch[1] == x;
}
void rotate(int x) {
int y = Tr[x].fa, z = Tr[y].fa, k = get(x);
if(!isRoot(y)) Tr[z].ch[get(y)] = x; Tr[x].fa = z;
Tr[y].ch[k] = Tr[x].ch[k ^ 1], Tr[Tr[x].ch[k ^ 1]].fa = y;
Tr[x].ch[k ^ 1] = y, Tr[y].fa = x;
pushUp(y), pushUp(x);
}
void splay(int x) {
update(x);
for(int fa = Tr[x].fa; !isRoot(x); rotate(x), fa = Tr[x].fa) {
if(!isRoot(fa)) rotate(get(fa) == get(x) ? fa : x);
}
}
其余辅助函数
void reverse(int x) {
swap(Tr[x].ch[0], Tr[x].ch[1]), Tr[x].reversed ^= 1;
}
void pushUp(int x) {
Tr[x].sum = Tr[Tr[x].ch[0]].sum ^ Tr[Tr[x].ch[1]].sum ^ Tr[x].value;
}
void pushDown(int x) {
if (!Tr[x].reversed) return;
reverse(Tr[x].ch[0]), reverse(Tr[x].ch[1]);
Tr[x].reversed = 0;
}
void update(int x) {
if (!isRoot(x)) update(Tr[x].fa);
pushDown(x);
}
完整代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 300000 + 10;
struct Node {
int fa, ch[2];
int sum, value;
bool reversed;
} Tr[MAXN];
int n, m, x, y, type;
bool isRoot(int x) {
return Tr[Tr[x].fa].ch[0] != x && Tr[Tr[x].fa].ch[1] != x;
}
void reverse(int x) {
swap(Tr[x].ch[0], Tr[x].ch[1]), Tr[x].reversed ^= 1;
}
void pushUp(int x) {
Tr[x].sum = Tr[Tr[x].ch[0]].sum ^ Tr[Tr[x].ch[1]].sum ^ Tr[x].value;
}
void pushDown(int x) {
if (!Tr[x].reversed) return;
reverse(Tr[x].ch[0]), reverse(Tr[x].ch[1]);
Tr[x].reversed = 0;
}
void update(int x) {
if (!isRoot(x)) update(Tr[x].fa);
pushDown(x);
}
bool get(int x) {
return Tr[Tr[x].fa].ch[1] == x;
}
void rotate(int x) {
int y = Tr[x].fa, z = Tr[y].fa, k = get(x);
if(!isRoot(y)) Tr[z].ch[get(y)] = x; Tr[x].fa = z;
Tr[y].ch[k] = Tr[x].ch[k ^ 1], Tr[Tr[x].ch[k ^ 1]].fa = y;
Tr[x].ch[k ^ 1] = y, Tr[y].fa = x;
pushUp(y), pushUp(x);
}
void splay(int x) {
update(x);
for(int fa = Tr[x].fa; !isRoot(x); rotate(x), fa = Tr[x].fa) {
if(!isRoot(fa)) rotate(get(fa) == get(x) ? fa : x);
}
}
void access(int u) {
for (int v = 0; u; v = u, u = Tr[u].fa)
splay(u), Tr[u].ch[1] = v, pushUp(u);
}
void makeRoot(int u) {
access(u), splay(u), reverse(u);
}
int findRoot(int u) {
access(u), splay(u);
pushDown(u); while (Tr[u].ch[0]) pushDown(u = Tr[u].ch[0]);
return u;
}
void link(int u, int v) {
makeRoot(v); if (findRoot(u) != v) Tr[v].fa = u;
}
void cut(int u, int v) {
makeRoot(u), access(v), splay(v);
if (Tr[v].ch[0] != u || Tr[Tr[v].ch[0]].ch[1]) return;
Tr[u].fa = Tr[v].ch[0] = 0, pushUp(v);
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &x);
Tr[i].value = Tr[i].sum = x;
}
while (m--) {
scanf("%d %d %d", &type, &x, &y);
switch (type) {
case 0 : makeRoot(x), access(y), splay(y), printf("%d\n", Tr[y].sum); break;
case 1 : link(x, y); break;
case 2 : cut(x, y); break;
case 3 : splay(x), Tr[x].value = y, pushUp(x); break;
}
}
}