【模板】Link Cut Tree (动态树)
题目链接:luogu P3690
题目大意
LCT 模板题。
要你实现树上的一些操作。
询问树上两点之间路径上点权的异或和。
连接两点,如果本来已经连通就不用。
删除两点之间的边,没有就不管。
更改某个点的权值。
思路
这道题是 LCT 的模板题。
它主要是用类似树链剖分的样子,把边分为虚边和实边(实边只有一个)。但是它判断哪个是实边的不用看子树大小的东西,它是你想改就该的。
为什么要弄这个呢?因为这样,树上两点的路径就变成了很多个实链,就可以一个一个处理。
然后对于每一条实链,我们把它弄成一个平衡树。然后中序遍历就是要按着这个链中每个节点的深度来排。也就是说,链顶就是放在最上面,链尾就是放在最后(就是最右的位置)。
那因为本来 splay 就是断开成几个,那我们可以只开一个结构体数组来装。
那首先,你看到要求的是路径异或和,那这个在 splay 里就是一个 up 向上传递的事。
然后这里先说一说,它还要翻转两个儿子,那就要弄个懒标记,为什么要用这个后面会说。
splay 的操作
接着,我们来看如何做 splay 的两个重要操作,rotate(翻转)和 splay(上提)。
对于 rotate,它每个东西的位置都很重要,然后还要记得上传父亲的值。
对于 splay,你在弄之前要先从上到下不停地下传懒标记。
access 函数
然后你想,你要求路径异或和,那你就要让两个点都在同一个平衡树上。那如果原来不在呢?
那我们就要弄个函数把它打通,使得两点之间有实链相连。这个函数就是 \(access\)。
它是怎么实现的呢?
我们这里只说打通一个点到原来的树根节点,也只会用到这个。
因为你后面还会有操作使得原来树的根节点更改。
它是这样的,我们先弄一个 \(son\) 记录它上一次的位置,然后我们把现在的它移到它平衡树的根的位置,然后它的右儿子就是 \(son\),就是把这条边改成实边。那你这个改成实边,原来的实边就会被替换掉,就变成了虚边。
但是为什么原来的实边是连向右儿子的呢?
因为你平衡树的中序遍历是按实链的深度排的,那你这样找是从深度大的到深度小的,那你的 \(son\) 的深度就比你现在的大,那根据你的平衡树的中序遍历的排法,它就应该放在你现在的右边。那按这个意思原来的实边也会在这个地方。
然后记得把值都维护一下,然后你就可以把 \(son\) 改成你现在的值,你现在的值走向它的父亲,找到下一个要和它合并的平衡树。
make_root 函数
那我们刚刚说了 \(access\) 函数只要打通一个点到根节点就可以,因为我们可以把一个点变成原来的树的根节点。
那怎么实现呢?
我们把实现的函数叫做 \(make_root\) 函数。
那我们可以这样,先把它和根打通(这个时候它是最后的),然后把它上提到根的位置,那它就是第一个了。
但是你想想这样会造成什么改变。
因为你是根又是第一个,那它就不会有左子树。
但是你一开始是最后,所以你是不会有右子树。
那简单,把左右儿子翻转即可。
那你想想就知道当然要一直翻转下去,那就需要 lazy 懒标记了。
当然为了保险,我用的是两个的那种,就是给某个点标记的时候它的两个儿子已经翻转好了的那种。如果写这个的话记得你在这里不能只标记 lazy,还要翻转儿子。
然后你也要记得及时把 lazy 下传。
select 函数
这个函数就是要把两点之间的路径搞出来。
那我们可以先把一个点设置为根,然后把另一个点跟它的路径打通,然后再选择到它平衡树的根,那它的左儿子就是原来树的根,也就是你前面的点了。
find_root 函数
那这个其实也还好,你先打通它到根的路,然后提上去。
然后因为根中序遍历在第一个,那它肯定在最左的地方,你就直接一直向左走,就找到了。
(记得下放)
link 函数
首先,你要判断它是否已经连通,就看它们的根是否相同。
如果相同,就可以直接跳过。
否则你就可以把一个点变成根,然后让它的父亲是另一个点。然后这条边是虚边。
(因为你没有必要让它是实边,后面要实边的话后面会改的)
delete_ 函数
删去一条边。
首先我们假设一定存在这条边。
那就把路径拎出来,然后把值改成 \(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;
}