【luogu P3690】【模板】Link Cut Tree (动态树)

【模板】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;
}

猜你喜欢

转载自blog.csdn.net/weixin_43346722/article/details/113932989
今日推荐