P3402 可持久化并查集 【可持久化线段树】详解

给定 n n n 个集合,第 i i i 个集合内初始状态下只有一个数,为 i i i
m m m 次操作。操作分为 3 3 3 种:

1 a b 合并 a , b a,b a,b 所在集合
2 k 回到第 k k k 次操作(执行三种操作中的任意一种都记为一次操作)之后的状态
3 a b 询问 a , b a,b a,b 是否属于同一集合,如果是则输出 1 1 1 ,否则输出 0 0 0

传送门

可持久化并查集模板

介绍

并查集通常来说是不可逆的,因为我们存在很多骚操作,让并查集的复杂度降到每次查询均摊 O ( 1 ) O(1) O(1),比如路径压缩
每次修改某些点的 f a t h e r father father信息,达到较快的速度查询
当然也不是一定不可逆,可撤销并查集就能让操作可逆,并查集能够后悔操作

可撤销并查集

这里大概说一下,可撤销并查集就是用结构体存储每次并查集路径压缩的前后的 f a t h e r father father值是什么,也就是只要某个点的 f a t h e r father father信息在压缩过程中改变了,就用结构体存起来,用栈维护修改序列
对于某次撤回操作,我们直接撤回到某个版本,当然我们也可以快进过去(用数组模拟栈的话比较好实现)
但是局限性是什么呢?
如果从版本 A A A,退回到某个版本 B B B后,(退回的代价是 O ( n ) O(n) O(n)),在 B B B这个版本上进行修改成为了版本 C C C A A A C C C都是属于 B B B的一个分支,在 A A A C C C之间随意切换的代价是, O ( n ) O(n) O(n),(用数组存结构体记录操作情况,操作加上版本信息,理论上能够实现)
这里劣势已经很明显了,可以初步得出可撤销并查集适用于短期内,撤销某次错误操作的情景

可持久化并查集

如果要在高频率访问操作时间轴上的任意版本的情况下,如何维护并查集呢?
发现,并查集的核心就是 f a t h e r father father数组,这里存储了每个点属于哪个集合
我们如果能够将 f a t h e r father father数组持久化下来就不能够实现,在多个版本随意切换了吗?

这里借用主席树的思想。
对于静态区间第 K K K大,我们记录的是前缀和,通过不同的版本进行差分得到某段区间,某个权值的个数。(稍微提一下,因为是前缀和,所以静态的不支持修改,修改需要在线树套树or离线整体二分)
但是我们这里不需要前缀和,我们只需要某个版本的信息就行了!
这可太好了,使用简单的主席树就能够维护啊。
对于某个版本,我每次只修改 l o g n logn logn个节点的信息,空间也满足了

具体是
初始建立一颗原始的主席树,每个节点的 f a t h e r father father都是自己
最后,根据主席树的操作,将修改链上的节点先克隆(clone),在克隆版本上进行修改,可知每次只需要修改 l o g n logn logn个节点,每次修改就是一个版本

修改 f a t h e r father father的操作通常在某些骚操作路径压缩按秩合并的时候发生
这里就出现了抉择,使用路径压缩还是按秩合并呢?(这里的按秩合并是按高度)
理论,两者皆可,反正我们能够维护每个版本的 f a t h e r father father信息
但是我们分析一下
对于路径压缩,在father[x] = find(father[x])的时候,会修改一次
对于某些特意构造的数据,就会造成某个版本需要修改很多次 f a t h e r father father,那么所有版本都是这么多修改的话,即使是动态开点线段树,也支撑不了如此巨大的内存消耗,所以在特殊情况下,极容易 M L E MLE MLE
对于按秩合并,由于没有为了刻意追求速度,将时间让步于空间,得到每次修改并查集只需要在主席树上修改一次的优秀性质

小细节注意:

  • b u i l d build build 里面检测 r t rt rt为0的时候调用了 c l o n e clone clone,其实每次都是克隆0号元素,等于说新建节点了,这里可以直接改为rt=++indx;
  • 代码里面 f i n d find find调用的 q u e r y query query并不是返回父亲的值就行了,因为我们还需要当前父亲的子树高度是多少(方便按秩合并),所以得返回存这个信息的节点编号。(如果仅仅只要看是否在同一集合中,返回父亲的值是可以的,具体情况不同)
  • 按秩合并,只有两个集合的高度/深度相同时,才需要对另一个进行高度修改。因为此时必然有一个挂在另一个的下面,高度应该加 1 1 1

代码

//P3402
/*
  @Author: YooQ
*/
#include <bits/stdc++.h>
using namespace std;
#define sc scanf
#define pr printf
#define ll long long
#define FILE_OUT freopen("out", "w", stdout);
#define FILE_IN freopen("in", "r", stdin);
#define debug(x) cout << #x << ": " << x << "\n";
#define AC 0
#define WA 1
#define INF 0x3f3f3f3f
const ll MAX_N = 1e6+5;
const ll MOD = 1e9+7;
int N, M, K;

int arr[MAX_N];

struct Tr {
	int fa, d, l, r;
}tr[MAX_N<<4];
int root[MAX_N];
int rcnt = 0;
int indx = 0;

int clone(int rt) {
	tr[++indx] = tr[rt];
	return indx;
}

void build(int& rt, int l, int r) {
	if (!rt) rt = clone(rt);
	if (l == r) {
		tr[rt].fa = l;
		tr[rt].d = 1;
		return;
	}
	int mid = l + ((r-l)>>1);
	build(tr[rt].l, l, mid);
	build(tr[rt].r, mid+1, r);
}

void update(int& rt, int l, int r, int x, int d) {
	rt = clone(rt);
	if (l == r) {
		tr[rt].d += d;
		return;
	}
	int mid = l + ((r-l)>>1);
	if (x <= mid) update(tr[rt].l, l, mid, x, d);
	if (x  > mid) update(tr[rt].r, mid+1, r, x, d);
}

int query(int rt, int l, int r, int x) {
	if (l == r) {
		return rt;
	}
	int mid = l + ((r-l)>>1);
	if (x <= mid) return query(tr[rt].l, l, mid, x);
	if (x  > mid) return query(tr[rt].r, mid+1, r, x);
}

int find(int rt, int x) {
	int y = query(rt, 1, N, x);
	return tr[y].fa == x ? y : find(rt, tr[y].fa);
}

void merge(int &rt, int l, int r, int x, int y) {
	rt = clone(rt);
	if (l == r) {
		tr[rt].fa = y;
		return;
	}
	int mid = l + ((r-l)>>1);
	if (x <= mid) merge(tr[rt].l, l, mid, x, y);
	if (x  > mid) merge(tr[rt].r, mid+1, r, x, y);
}

void solve(){
	sc("%d%d", &N, &M);
	build(root[0], 1, N);
	int opt, x, y;
	for (int i = 1; i <= M; ++i) {
		sc("%d", &opt);
		root[i] = root[i-1];
		if (opt == 1) {
			sc("%d%d", &x, &y);
			x = find(root[i], x);
			y = find(root[i], y);
			if (tr[x].fa == tr[y].fa) continue;
			if (tr[x].d > tr[y].d) swap(x, y);
			merge(root[i], 1, N, tr[x].fa, tr[y].fa);
			if (tr[x].d == tr[y].d) {
				update(root[i], 1, N, tr[y].fa, 1);
			}
		} else if (opt == 2) {
			sc("%d", &x);
			root[i] = root[x];
		} else {
			sc("%d%d", &x, &y);
			x = find(root[i], x);
			y = find(root[i], y);
			if (tr[x].fa == tr[y].fa) {
				puts("1");
			} else {
				puts("0");
			}
		}
	}
}

signed main()
{
	#ifndef ONLINE_JUDGE
	//FILE_IN
	FILE_OUT
	#endif
	int T = 1;//cin >> T;
	while (T--) solve();

	return AC;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hexrt

客官,请不要给我小费!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值