可持久化并查集

相信所有人都学过可持久化线段树 & 并查集吧(没学过的请出门右转,因为我太懒了没写这两篇)。

好吧没学过并查集的点这里

正片开始

【模板】可持久化并查集
大概就是说给你 \(n\) 个点,需要进行 \(m\) 次以下操作:

  • 合并 \(a\) 所在的集合与 \(b\) 所在的集合;
  • 回到版本 \(k\),也就是第 \(k\) 次操作后的版本;
  • 查询 \(a\)\(b\) 是否在一个集合内;

抛开操作二不谈,这就是一个普普通通的并查集板子。但现在加上了这个神人操作,我们应该怎么办呢?
很容易联想到可持久化线段树,那么把这玩意和并查集合起来就成了。
具体来说,并查集最重要的就是 \(father\) 数组,于是乎我们需要将这东西可持久化。
那就用上刚刚提到的可持久化线段树了。

考虑时间复杂度:不加优化很显然是慢得爆炸的。
如何优化呢?
路径压缩绝对不行,不信可以试试看,会 \(T\) 的飞起。因为路径压缩在普通并查集中的复杂度是均摊的,由于某些神秘的原因,在可持久化并查集中它有可能会退化。

那用啥呢?
那就不得不提起另一个东西了——按秩合并
顾名思义,指的就是“按照一定的秩序进行合并操作”(但我以前一直以为是“暗质合并”)。
有以下几个选项:

  • 按照子树深度
  • 按照子树大小

其实可以随机,但是随机被卡了,所以我们考虑使用以上两种之一。
其实很好理解,你考虑把大小或者深度更小的子树合并到大子树上去,肯定比反过来更优(可以感性理解一下)。
我喜欢按子树深度合并,当把深度小的合并到深度大的上面时,很容易发现深度要么保持不变要么就加一。

思想很简单,代码很复杂。

\(Code\)

在此声明:代码全是我手打的,旁边的 \(zyx\) 貌似认为我的代码很人机。

#include<bits/stdc++.h>
using namespace std;
char *p1,*p2,buf[100000];
#define nc() (p1==p2 && (p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++)
int read(){
    int x=0,f=1;
    char ch=nc();
    while(ch<48||ch>57){
        if(ch=='-') f=-1;
        ch=nc();
    }
    while(ch>=48&&ch<=57) x=x*10+ch-48,ch=nc();
   	return x*f;
}
void write(long long x){
    if(x<0) putchar('-'),x=-x;
    if(x>9) write(x/10);
    putchar(x%10+'0');
	putchar('\n');
    return;
} // 快读快写的板子  
struct node{
	int left_son,right_son,father,deep;
};
node tree[6000005];
int n,m,cnt=0,root[300005];
int build(int L,int R){
	int tot=++cnt; // 创建新版本 
	if(L==R){
		tree[tot].father=L,tree[tot].deep=1;
		return tot;
	}
	int mid=(L+R)/2; // 左右两边继续  
	tree[tot].left_son=build(L,mid),tree[tot].right_son=build(mid+1,R);
	return tot;
}
int query(int now,int L,int R,int u){
	while(L!=R){
		int mid=(L+R)/2;
		if(u<=mid) now=tree[now].left_son,R=mid;
		else now=tree[now].right_son,L=mid+1;
	}
	return now;
}
int find(int now,int u){
	while(1){
        int now_father=query(root[now],1,n,u); // 查找 u 的父亲  
        if(tree[now_father].father==u) return now_father; // 找到了  
        u=tree[now_father].father;
    }
}
int copy(int now){ // 拷贝过来  
	int tot=++cnt;
	tree[tot]=tree[now];
	return tot;
}
int merge(int now,int L,int R,int pos,int pos_father){
	int tot=copy(now);
	if(L==R){
		tree[tot].father=pos_father; // 小小合并  
		return tot;
	}
	int mid=(L+R)/2;
	if(pos<=mid) tree[tot].left_son=merge(tree[now].left_son,L,mid,pos,pos_father);
	else tree[tot].right_son=merge(tree[now].right_son,mid+1,R,pos,pos_father);
	return tot;
}
void change(int now,int a,int b){
	root[now]=root[now-1]; // 创建新版本  
	a=find(now,a),b=find(now,b); // 找俩父亲  
	if(tree[a].father!=tree[b].father){ // 不在一个集合里  
		if(tree[a].deep>tree[b].deep) swap(a,b); //深度小的合并到深度大的上  
		root[now]=merge(root[now-1],1,n,tree[a].father,tree[b].father);
		if(tree[a].deep==tree[b].deep){ // 深度相同合并后要更新  
			int pos=tree[b].father;
			root[now]=merge(root[now],1,n,pos,pos); // 复制节点
			int _new=query(root[now],1,n,pos);
			tree[_new].deep++;
		}
	}
}
bool decision(int now,int a,int b){ // 判断  
	a=find(now,a),b=find(now,b);
	if(tree[a].father==tree[b].father) return true; 
	else return false;
}
int main(){
	n=read(),m=read();
	root[0]=build(1,n);
	for(int i=1;i<=m;i++){
		int opt;
		opt=read();
		if(opt==2){
			int k=read();
			root[i]=root[k]; // ban过来 
		}
		else if(opt==1){
			int a=read(),b=read();
			change(i,a,b); // 修改  
		}
		else{
			int a=read(),b=read();
			write(decision(i-1,a,b));
			root[i]=root[i-1]; // 查询一次也算操作,要建新版本哦喵  
		} 
	}
	return 0;
}

\(-\) ❀ 完结撒花 ❀ \(-\)

posted @ 2026-03-26 16:47  404-NoT_FinD  阅读(17)  评论(0)    收藏  举报