Link Cut Tree 是一种用来维护动态树问题的数据结构。
其维护的是一个森林,森林中的每个树由若干个 Splay 组成,每个 Splay 代表树上的一条链,一个 Splay 的中序遍历就是那条链的顺序。
为了将一棵树分成若干条链,我们引入实链剖分。
实链剖分
实链剖分的定义与重链剖分十分相似。
树上的所有边被分成了实边和虚边,一个边的虚实是动态制定的。
且一个点到其儿子的所有边中最多只有一条实边。
LCT 的操作
LCT 中的所有操作都是以链为基本单位的,由于我们使用 Splay 来维护链,使得 LCT 具有了极为强大的可扩展性。
Splay 森林如何组成一棵树
我们采用一种认父不认虚儿子的方法来组成一棵树。
具体的,Splay 的根节点会指向这条链的父亲,但是这条链的父亲并不会认这个儿子,因为他们是在不同的 Splay 中的。
所以说,一个节点的实儿子最多只有两个,一个点的虚儿子可能有很多个。
如何改变虚实关系
因为 LCT 中的所有操作都是以链为基本单位进行的,所以有的时候我们可能需要把一条从根到某个节点的路径变为实链,这就引入了 LCT 中最重要的操作 Access
。
注意:Access 操作是在一个树内(Splay 森林)中进行的
记我们要将根节点到 \(x\) 到路径变为实链。
首先我们要将 \(x\) 的指向儿子的边变为虚边。
这是因为我们之后可能要执行换根操作,如果不将这几条边变为虚边,将 x 换做根后就违背了实链剖分的定义
然后将 \(x\) Splay
到当前 Splay 的根。
设为根后我们要将他的父亲的右儿子设为 \(x\),我们记其父亲为 \(p\)。
由于认父不认虚儿子的原则,我们进行这一步其实就是将 x 设为实儿子
然后我们再将 \(p\) Splay 到根,继续进行下去,直到我们的 \(p = 0\)。
p = 0 实际上就意味着当前点没有父亲了,因为在 LCT 中任意两个树之间其实是没有联系的,所以一个 Splay 森林中唯一的那一个没有父亲的点就是那棵树到根
这样我们就完成了 Access(x)
的操作。
同时别忘了我们在 Splay 过后要进行更新操作,每个 Splay 的根其实就代表了其整条链的信息。
换根
假设我们要访问两个点之间的路径的答案时,只有 Access
操作是不够的,所以我们就引入了 MakeRoot
操作。
MakeRoot
操作是将一个节点 \(x\) 换成这棵树的根。
其实这个操作是比较简单的。
我们可以先用 Access
操作将当前根和 \(x\) 弄到一个 Splay 中,然后再用 Splay
操作将 \(x\) 换到根。
注意 MakeRoot 操作是将 x 变为树的根而不只是 Splay 的根
接下来我们考虑如何修正边的关系。
因为我们之前保证了 Splay 的中序遍历一定是按深度关系排序的,但由于我们换根之后深度关系应该倒置了,所以我们需要将这个中序遍历 reverse 一下。
其实这也是 LCT 选择 Splay 的一个原因,我们可以效仿文艺平衡树中的做法,给当前节点打一个 reverse 标记,然后翻转左右节点即可。
找根
找根也是比较简单的。
首先我们和换根操作一样先将 \(x\) 变为 Splay 的根,即执行 Access
和 Splay
二连。
然后又因为树的根节点是深度最小的,所以我们只要一直走左儿子就好了,不要忘记下穿翻转标记。
找到根后记得将树根 Splay 到根来保证复杂度。
拎出一条路径
有了之前的铺垫,这一个操作也是比较简单的。
我们先 MakeRoot(x)
然后将根(\(x\))到 \(y\) 的路径打通,即调用 Access(y)
,然后为了保证复杂度我们还要 Splay(y)
,最后这条路径的信息就都在 y
上了。
连边
我们先将 \(x\) 换根,然后再在 \(x\) 向 \(y\) 连一条虚边就好了(\(x\) 做儿子,因为 \(x\) 是根)。
记得判定非法情况,即换根后发现 \(y\) 的根是 \(x\)。
删边
我们先将 \(x\) 换根。
然后考虑什么情况下不能删边。
我们应该先判定是否在一个树内,再判定他们是不是父子关系。
然后就可以删边了,因为我们在判定中需要调用 FindRoot
操作,而 FindRoot
操作又要掉用 Access
操作,所以 \(y\) 和 \(x\) 之间连的一定是一条实边,不要忘记删掉 \(x\) 的儿子信息。
更新权值
我们要将 \(x\) 先 Splay 到根,这样我们就可以直接更新其权值而不会影响到其他节点了。
复杂度分析
还不会,长大后再学习,毕竟我连 Splay 的复杂度都还不会分析。
代码
注意事项写代码里了,还是总结下吧。
- 注意 link 操作是从 x 向 y 连边,应该是 \(fa_x = y\) 不要写反了。
- 注意 splay 里循环条件是
!is_splay_root(x)
而里面判断是否要 zig-zig 的条件是!is_splay_root(f)
。 - 注意 link 和 cut 里判断是否在一个子树的代码不要写成
find_root(x) == y
,应该写find_root(y) == x
。
#include <cstdio>
#include <algorithm>
#include <functional>
using std::function;
/**
*
* Luogu P3690
*
* Maintain Path bitwise-xor sum
*
* LCT Template By luanmenglei
*
**/
namespace LCT {
using std::swap;
const int N = 1e5 + 10;
int ch[N][2], fa[N], val[N], sum[N], sze[N], tag[N];
void update(int x) {
sum[x] = sum[ch[x][0]] ^ sum[ch[x][1]] ^ val[x];
sze[x] = sze[ch[x][0]] + sze[ch[x][1]] + 1;
}
bool is_splay_root(int x) {
return ch[fa[x]][0] != x && ch[fa[x]][1] != x;
}
int get(int x) {
return ch[fa[x]][1] == x;
}
void pushdown(int x) {
if (tag[x]) {
tag[x] = 0;
auto operate = [&](int y) { // maybe the only thing you need to modify in different problem
tag[y] ^= 1;
swap(ch[y][0], ch[y][1]);
};
if (ch[x][0]) operate(ch[x][0]);
if (ch[x][1]) operate(ch[x][1]);
}
}
void pushdown_all(int x) {
if (!is_splay_root(x)) pushdown_all(fa[x]);
pushdown(x);
}
void set(int f, int x, int side) { ch[f][side] = x; fa[x] = f; }
void rotate(int x) {
int y = fa[x], z = fa[y], side_x = get(x), side_y = get(y);
fa[x] = z;
if (!is_splay_root(y)) ch[z][side_y] = x;
set(y, ch[x][side_x ^ 1], side_x), set(x, y, side_x ^ 1);
ch[0][0] = ch[0][1] = fa[0] = 0;
update(y), update(x), update(z); // update by depth's other
sum[0] = 0;
}
void splay(int x) { // just like the normal splay
pushdown_all(x);
for (int f = fa[x], g = fa[f]; !is_splay_root(x); rotate(x), f = fa[x], g = fa[f])
if (!is_splay_root(f))
rotate(get(f) == get(x) ? f : x);
}
void access(int x) {
for (int y = 0; x; y = x, x = fa[x])
splay(x), ch[x][1] = y, update(x);
}
void make_root(int x) {
access(x), splay(x);
tag[x] ^= 1;
swap(ch[x][0], ch[x][1]);
}
int find_root(int x) {
access(x), splay(x);
while (ch[x][0]) pushdown(x), x = ch[x][0];
return splay(x), x;
}
bool link(int x, int y) { // return is success
make_root(x);
if (find_root(y) == x) return false;
else return fa[x] = y, true; // remember this operation is set x's father to y !!!!
}
bool cut(int x, int y) { // return is success
make_root(x);
if (find_root(y) != x || fa[y] != x || ch[x][1] != y || ch[y][0]) return false;
return fa[y] = 0, ch[x][1] = 0, update(x), true;
}
void split(int x, int y) {
make_root(x);
access(y);
splay(y);
}
}
using LCT::link;
using LCT::cut;
using LCT::split;
using LCT::val;
using LCT::sum;
using LCT::splay;
using LCT::update;
const function<void(int, int)> LCT_FUNCS[] = {
[](int x, int y) {
split(x, y);
printf("%d\n", sum[y]);
},
[](int x, int y) {
link(x, y);
},
[](int x, int y) {
cut(x, y);
},
[](int x, int value) {
splay(x);
val[x] = value;
update(x);
}
};
int main() {
int n, q;
scanf("%d%d", &n, &q);
for (int i = 1; i <= n; i ++) scanf("%d", &val[i]);
while (q --) {
int op, x, y; scanf("%d%d%d", &op, &x, &y);
LCT_FUNCS[op](x, y);
}
return 0;
}