【模板】Link Cut Tree (动态树)
题目链接:luogu P3690
题目大意
LCT 模板题。
要你实现树上的一些操作。
询问树上两点之间路径上点权的异或和。
连接两点,如果本来已经连通就不用。
删除两点之间的边,没有就不管。
更改某个点的权值。
思路
这道题是 LCT 的模板题。
它主要是用类似树链剖分的样子,把边分为虚边和实边(实边只有一个)。但是它判断哪个是实边的不用看子树大小的东西,它是你想改就该的。
为什么要弄这个呢?因为这样,树上两点的路径就变成了很多个实链,就可以一个一个处理。
然后对于每一条实链,我们把它弄成一个平衡树。然后中序遍历就是要按着这个链中每个节点的深度来排。也就是说,链顶就是放在最上面,链尾就是放在最后(就是最右的位置)。
那因为本来 splay 就是断开成几个,那我们可以只开一个结构体数组来装。
那首先,你看到要求的是路径异或和,那这个在 splay 里就是一个 up 向上传递的事。
然后这里先说一说,它还要翻转两个儿子,那就要弄个懒标记,为什么要用这个后面会说。
splay 的操作
接着,我们来看如何做 splay 的两个重要操作,rotate(翻转)和 splay(上提)。
对于 rotate,它每个东西的位置都很重要,然后还要记得上传父亲的值。
对于 splay,你在弄之前要先从上到下不停地下传懒标记。
access 函数
然后你想,你要求路径异或和,那你就要让两个点都在同一个平衡树上。那如果原来不在呢?
那我们就要弄个函数把它打通,使得两点之间有实链相连。这个函数就是 a c c e s s access access。
它是怎么实现的呢?
我们这里只说打通一个点到原来的树根节点,也只会用到这个。
因为你后面还会有操作使得原来树的根节点更改。
它是这样的,我们先弄一个 s o n son son 记录它上一次的位置,然后我们把现在的它移到它平衡树的根的位置,然后它的右儿子就是 s o n son son,就是把这条边改成实边。那你这个改成实边,原来的实边就会被替换掉,就变成了虚边。
但是为什么原来的实边是连向右儿子的呢?
因为你平衡树的中序遍历是按实链的深度排的,那你这样找是从深度大的到深度小的,那你的 s o n son son 的深度就比你现在的大,那根据你的平衡树的中序遍历的排法,它就应该放在你现在的右边。那按这个意思原来的实边也会在这个地方。
然后记得把值都维护一下,然后你就可以把 s o n son son 改成你现在的值,你现在的值走向它的父亲,找到下一个要和它合并的平衡树。
make_root 函数
那我们刚刚说了 a c c e s s access access 函数只要打通一个点到根节点就可以,因为我们可以把一个点变成原来的树的根节点。
那怎么实现呢?
我们把实现的函数叫做 m a k e r o o t make_root makeroot 函数。
那我们可以这样,先把它和根打通(这个时候它是最后的),然后把它上提到根的位置,那它就是第一个了。
但是你想想这样会造成什么改变。
因为你是根又是第一个,那它就不会有左子树。
但是你一开始是最后,所以你是不会有右子树。
那简单,把左右儿子翻转即可。
那你想想就知道当然要一直翻转下去,那就需要 lazy 懒标记了。
当然为了保险,我用的是两个的那种,就是给某个点标记的时候它的两个儿子已经翻转好了的那种。如果写这个的话记得你在这里不能只标记 lazy,还要翻转儿子。
然后你也要记得及时把 lazy 下传。
select 函数
这个函数就是要把两点之间的路径搞出来。
那我们可以先把一个点设置为根,然后把另一个点跟它的路径打通,然后再选择到它平衡树的根,那它的左儿子就是原来树的根,也就是你前面的点了。
find_root 函数
那这个其实也还好,你先打通它到根的路,然后提上去。
然后因为根中序遍历在第一个,那它肯定在最左的地方,你就直接一直向左走,就找到了。
(记得下放)
link 函数
首先,你要判断它是否已经连通,就看它们的根是否相同。
如果相同,就可以直接跳过。
否则你就可以把一个点变成根,然后让它的父亲是另一个点。然后这条边是虚边。
(因为你没有必要让它是实边,后面要实边的话后面会改的)
delete_ 函数
删去一条边。
首先我们假设一定存在这条边。
那就把路径拎出来,然后把值改成 0 0 0。
原来树的根的点的父亲变成 0 0 0,另一个点的左儿子变成 0 0 0。
至于为什么是左儿子……跟上面一个道理。
但是现在问题就是如何判断有没有这条边。
那如果这条边存在,那它会在实链中的一个地方出现。
那两个点的中序遍历就是相邻的。
那因为它们分别是原树的根和平衡树的根,那原树的根应该是平衡树的根的左儿子,它的父亲就是平衡树的根。
那当然,它们还要在同一个连通块。
接着你来看如何保证中序遍历相邻,或者说,要怎么样才不会相邻。
那你会想到如果原树的根有右儿子,那遍历到原树的根之后就会遍历到它的右儿子而不是平衡树的根。那就是这个情况了,那就要原树的根没有右儿子。
关于查询操作
那你考虑把它们之间的路径拎出来。
那因为这个路径就是那一条链,然后我们维护了异或和。那我们就可以直接输出这个路径的根节点里面带有的异或和,就可以了。
关于修改点权
要先把这个点提上去,再修改。
不然 Splay 信息会不正确,你提上去就只有它的异或和要改。
说的可能不是很懂,可以结合代码看看。
代码
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
struct Tree {
int son[2], fa, val, lazy;
}tree[200001];
int n, m, root, tot, val[200001];
int tmp[200001], x, y, op;
string s;
int read() {
//快读
int re = 0;
char c = getchar();
while (c < '0' || c > '9') c = getchar();
while (c >= '0' && c <= '9') {
re = re * 10 + c - '0';
c = getchar();
}
return re;
}
bool is_root(int now) {
//判断是否是当前所在平衡树中的根节点
return tree[tree[now].fa].son[0] != now && tree[tree[now].fa].son[1] != now;
}
void down1(int now) {
//向下传递lazy
swap(tree[now].son[0], tree[now].son[1]);
tree[now].lazy ^= 1;
}
void down(int now) {
//向下传递lazy是就先把子树恢复好,可以避免因为询问儿子的位置关系而出问题
if (tree[now].lazy && now) {
if (tree[now].son[0]) down1(tree[now].son[0]);
if (tree[now].son[1]) down1(tree[now].son[1]);
tree[now].lazy = 0;
}
}
void up(int now) {
//向上传递异或和的值
tree[now].val = tree[tree[now].son[0]].val ^ tree[tree[now].son[1]].val ^ val[now];
}
void rotate(int now) {
//旋转
int father = tree[now].fa;
int grand = tree[father].fa;
int k = (now == tree[tree[now].fa].son[0]);
int son = tree[now].son[k];
if (!is_root(father))
tree[grand].son[father == tree[tree[father].fa].son[1]] = now;
tree[now].son[k] = father;
tree[father].son[!k] = son;
tree[father].fa = now;
tree[now].fa = grand;
if (son) tree[son].fa = father;
up(father);
}
void splay(int x) {
//上提
tmp[0] = 1;
tmp[1] = x;
for (int i = x; !is_root(i); i = tree[i].fa)
tmp[++tmp[0]] = tree[i].fa;
while (tmp[0]) {
//先从上到下下方懒标记
down(tmp[tmp[0]]);
tmp[0]--;
}
while (!is_root(x)) {
if (!is_root(tree[x].fa)) {
rotate(((x == tree[tree[x].fa].son[1]) ^ (tree[x].fa == tree[tree[tree[x].fa].fa].son[1])) ? x : tree[x].fa);
}
rotate(x);
}
up(x);
}
void access(int now) {
//打通两点之间的路径,就是把中间的边全部变成实边,然后再把原来的实边改成虚边
int son = 0;
while (now) {
splay(now);
tree[now].son[1] = son;//改成实边,原来的实边被替换掉,就自动变成了虚边
up(now);
if (son) tree[son].fa = now;
son = now;
now = tree[now].fa;
}
up(now);
}
int find_root(int now) {
//找到它所在平衡树的根
access(now);
splay(now);
down(now);
while (tree[now].son[0]) {
//根一定在最左边
now = tree[now].son[0];
down(now);
}
return now;
}
void make_root(int now) {
//把它弄成题目中的树的根
access(now);
splay(now);
down1(now);
}
void select(int x, int y) {
//提取出两点之间的路径
make_root(x);
access(y);
splay(y);
}
void link(int x, int y) {
//连边
if (find_root(y) == find_root(x)) return ;//原来已经连通
make_root(x);
tree[x].fa = y;
}
void delete_(int x, int y) {
//删边
select(x, y);
if (find_root(y) == x && tree[x].fa == y && tree[y].son[0] == x && !tree[x].son[1]) {
//本来要有边
tree[x].fa = 0;
tree[y].son[0] = 0;
}
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &val[i]);
tree[i].val = val[i];
}
for (int i = 1; i <= m; i++) {
scanf("%d", &op);
scanf("%d %d", &x, &y);
if (op == 0) {
select(x, y);
printf("%d\n", tree[y].val);
}
else if (op == 1) {
link(x, y);
}
else if (op == 2) {
delete_(x, y);
}
else if (op == 3) {
splay(x);
val[x] = y;
}
}
return 0;
}