Appearance
动态树是用于解决“动态”树上问题的数据结构,即维护森林。常见的动态树有三种:Link-Cut-Tree,Euler Tour Tree,Top Tree。
其中:
- Link-Cut-Tree 用于解决路径问题。
- Euler-Tour-Tree 用于解决子树问题。
- Top-Tree 用于解决路径、子树问题。
由于 ETT 和 Top-Tree 的编码难度,一般仅使用 LCT。
Link-Cut-Tree
简单来说,LCT 就是用序列 Splay 维护树链,与树链剖分不同,因为 LCT 支持修改树形态,所以是任意链剖分,不是轻重链剖分。
将一条链视作一个序列,将链上节点的深度作为 Splay 的 Key。
一般而言,将每一条链的 Splay 视作一个节点后,形成的 LCT 视作结构树,Splay 视作节点树。
LCT 在维护图上,和并查集类似,维护的是森林,不能维护无向图。
access(x):
access 是 LCT 的核心操作,其直接目的是构造一条从
对于当前
cpp
int access(int x) {
int y = 0, z = x;
for (; x; y = x, x = tree.tr[x].fa) {
tree.splay(x);
tree.rs(x) = y;
tree.push_up(x);
}
tree.splay(z);
return y;
}isroot(x):
LCT 节点树的 Splay 与常规 Splay 略有区别,因为 LCT 有多棵 Splay,所以将节点转到 Splay 根时,是转到其所在节点树的根。但是节点树 Splay 内根节点要储存在结构树上的父节点。所以节点树 Splay 还需要一个判断是否是节点树根的函数。
cpp
bool isroot(int p) {
return ls(tr[p].fa) != p && rs(tr[p].fa) != p;
}make_root(x):
make_root 是将 access(y) 打通
具体实现,只要 access(x),此时
cpp
void make_root(int x) {
access(x);
tree.rev(x);
}LCT 核心操作,只有上述 access(x) 和 make_root(x) 两个,还有一些扩展操作。
find_root(x):
LCT 是一片森林,find_root 是找到 access(x) 后,
cpp
int find_root(int x) {
access(x);
while (tree.ls(x)) tree.push_down(x), x = tree.ls(x);
tree.splay(x);
return x;
}split(x,y):
split 就是前文中提到的,将
cpp
void split(int x, int y) {
make_root(x);
access(y);
}link(x,y):
将森林的两棵树连接起来,若保证
cpp
void link(int x, int y) {
make_root(x);
tree.tr[x].fa = y;
}若不保证
cpp
void link(int x, int y) {
make_root(x);
make_root(y);
tree.tr[x].fa = y;
}cut(x,y):
将连接 make_root(x),再 access(y),因为
cpp
void cut(int x, int y){
make_root(x);
access(y);
tree.ls(y) = tree.tr[x].fa = 0;
}若不保证
cpp
void cut(int x, int y) {
make_root(x);
if (find_root(y) == x && tree.tr[y].fa == x && !tree.ls(y)) {
tree.rs(x) = tree.tr[y].fa = 0;
}
}注意以上两者的区别。Splay 每次操作一个节点后,都要将其转到根,会影响节点树上的父子关系。
LCT 中的 Splay:
如下:
cpp
struct node {
int s[2];
int v, fa;
int sum, rev;
};
struct Splay {
node tr[N];
vector<int> q;
void init(int n, vector<int> &a) { // 每个节点作为孤立点初始化
for (int i = 1; i <= n; i++) {
tree.tr[i].sum = tree.tr[i].v = tree.tr[i].sz = 1;
}
}
void rev(int p) {
swap(ls(p), rs(p));
tr[p].rev ^= 1;
}
void push_up(int p) { // 以具体 push_up 维护信息为准
tr[p].sum = (tr[ls(p)].sum ^ tr[rs(p)].sum ^ tr[p].v);
}
void push_down(int p) {
if (tr[p].rev) {
rev(ls(p)), rev(rs(p));
tr[p].rev = 0;
}
}
bool isroot(int p) { return ls(tr[p].fa) != p && rs(tr[p].fa) != p; }
void rorate(int x) {
int y = tr[x].fa, z = tr[y].fa;
bool w = (rs(y) == x);
if (!isroot(y))
tr[z].s[rs(z) == y] = x;
tr[x].fa = z;
tr[y].s[w] = tr[x].s[w ^ 1], tr[tr[x].s[w ^ 1]].fa = y;
tr[x].s[w ^ 1] = y, tr[y].fa = x;
push_up(y), push_up(x);
}
void splay(int x) {
int r = x;
q.clear();
q.push_back(r);
while (!isroot(r)) {
q.push_back(tr[r].fa);
r = tr[r].fa;
}
int len = q.size();
len--;
while (len >= 0)
push_down(q[len--]);
while (!isroot(x)) {
int y = tr[x].fa, z = tr[y].fa;
if (!isroot(y)) {
if ((rs(y) == x) ^ (rs(z) == y))
rorate(x);
else
rorate(y);
}
rorate(x);
}
}
};LCT 的替代品:
在允许离线的情况下,LCT 有许多替代品。
树链剖分:
因为动态树维护的是森林,所以在操作结束后,若建立一个虚根把所有树接到这个虚根上,那么会形成一棵树。
若操作过程中只涉及加边,而不涉及删边(也就是对于最后的树而言,树的形态是确定的),那么对于询问中的路径问题,可以转化成离线后的树上路径问题,使用树链剖分(或根据实际询问内容,选择合适的方式维护即可)维护即可。
但是时间复杂度是
线段树分治+可撤销并查集:
若询问内容和连通性有关,或是并查集能够维护的子树信息。那么可以将操作过程中,对某一条边的 link,cut 视作在操作序列时间轴上的一段存在后消失的边。使用线段树分治+可撤销并查集维护即可。
但是时间复杂度是
全局平衡二叉树
感觉主要用于处理动态 DP 问题,数据结构上不是很有不可替代性,暂略。
