可撤销并查集学习笔记
前言
今天在吃饭的时候听 @Kenma 讲解了一遍,发现好像很好用而且很好写,就写了。
思路
拿栈存每次操作时被合并的父亲节点,撤销时直接将其指回自己,所以不能使用路径压缩,使用按秩合并。
只能离线。
可持久化并查集
发现如果访问某个历史版本时采用一步一步撤销回去太慢了。所以我们建立一棵版本树。
形象地说:想象一条以时间为节点的链,上面连着几条跨越时间的边,当我们访问到某个时间节点时,如果他有跨越时间的边,直接访问,然后正常进行操作即可。(有没有感觉很优美而且很有意境,学魔怔了)
具体而言:如果时间点 \(i\) 不是操作 2 ,那就让时间点 \(i\) 向 \(i+1\) 连边,时间照常流转;如果时间点 \(i\) 是操作 2 ,回到 \(a_i\) 的时间点,那就让时间点 \(a_i\) 向 \(i\) 连边(过去向未来连),因为我们需要的是 \(a_i\) 的版本。
code:
点击查看代码
#include<iostream>
using namespace std;
const int N=2e5+10;
int n,m;
int op[N],a[N],b[N],tim[N],ans[N];
struct Edge{
int to,nxt;
}e[2*N];
int head[N],tot=1;
void add(int u,int v){
e[++tot].to=v;
e[tot].nxt=head[u];
head[u]=tot;
return ;
}
int fa[N],siz[N],sta[N],top;
void init(){
for(int i=1;i<=n;i++){
fa[i]=i,siz[i]=1;
}
return;
}
int find(int x){
if(x==fa[x]) return x;
return find(fa[x]);
}
void merge(int x,int y){
int a=find(x),b=find(y);
if(a==b) return ;
if(siz[a]>siz[b]) fa[b]=a,siz[a]+=siz[b],sta[++top]=b;
else fa[a]=b,siz[b]+=siz[a],sta[++top]=a;
return ;
}
void goback(int t){
while(top>t){
int y=sta[top--];
siz[fa[y]]-=siz[y];
fa[y]=y;
}
return ;
}
void dfs(int u){
tim[u]=top;
if(op[u]==1) merge(a[u],b[u]);
if(op[u]==3) ans[u]=(find(a[u])==find(b[u]))?1:0;
for(int i=head[u];i;i=e[i].nxt){
dfs(e[i].to);
}
goback(tim[u]);
}
int main(){
cin>>n>>m;
init();
for(int i=1;i<=m;i++){
cin>>op[i]>>a[i];
if(op[i]!=2) cin>>b[i],add(i-1,i);
else add(a[i],i);
}
dfs(0);
for(int i=1;i<=m;i++){
if(op[i]==3) cout<<ans[i]<<'\n';
}
return 0;
}