数据结构并查集
Akiyama_mzk · · 算法·理论
\texttt{part 1} 朴素的并查集
引入
并查集是一种用于管理元素所属集合的 数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素。
你需要写一个数据结构实现以下两种操作
1: 合并 x,y 所在的集合。
2: 询问 x y 是否处于同一个集合。
顾名思义,并查集支持两种操作:
合并
查询
但是,并查集无法以较低复杂度实现集合的分离。
初始化
初始时,每个元素都位于一个单独的集合,表示为一棵只有根节点的树。方便起见,我们将根节点的父亲设为自己。
for(int i=1;i<=n;i++){
fa[i]=i;
}
查询
我们需要沿着树向上移动,直至找到根节点。
return fa[x]==x?x:find(fa[x]);
合并
将一棵树连同根节点一块并到另一棵树上。
int uni(int x,int y){
return fa[find(x)]=find(y)
}
\texttt{part 2} 路径压缩与按秩合并
路径压缩
查询过程中经过的每个元素都属于该集合,我们可以将其直接连到根节点以加快后续查询。
return fa[x]==x?x:fa[x]=find(fa[x]);
按秩合并
合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。我们可以将节点较少或深度较小的树连到另一棵,以免发生退化。
如果只使用按秩合并,而不使用路径压缩,时间复杂度为
int uni(int x,int y){
int fx=find(x);
int fy=find(y);
if(fx==fy){
return;
}
if(sized[fx]<sized[fy]){
swap(x, y);
}
fa[fy]=fx;
sized[x]+=sized[y];
}
复杂度
时间复杂度
空间复杂度
显然为
\texttt{part 3} 带权并查集
我们还可以在并查集的边上定义某种权值、以及这种权值在路径压缩时产生的运算,从而解决更多的问题。
比如对于经典的 P1196 [NOI2002] 银河英雄传说。
我们可以用并查集实现,题目要求我们计算
数组
路径压缩时:
并查集记录子树的大小。考虑统计每次操作的贡献。如果第
杂题 2 : 并查集生成树
题面
有
接下来有
接下来有
\texttt{solution}
考虑在并查集合并的时候记录并查集生成树,也就是说如果第
杂题 3 : 并查集离线
题面
有
接下来有
接下来有
\texttt{solution}
离线算法:考虑将询问按
杂题 4 : 并查集与序列位置操作
题面
给一个长度为
-
令
a_x=1 -
求
a_x,a_{x+1},\ldots,a_n 中左数第一个为0 的位置。
\texttt{solution}
建立一个并查集,
对于一次
时间复杂度
杂题 5 : 并查集联通块与序列操作
题面
给出三个长度为
\texttt{solution}
按权值从大到小考虑
杂题 6 : 并查集与双联通分量
题面
给出一棵
加一条从
\texttt{solution}
询问可以转化为:求
换言之,加边操作可以理解为,将
考虑使用并查集维护。给树定根,设
杂题 7 : 并查集与缩点与桥
题面
无向图
接下来有
每次操作后,你均需要求出图中桥的个数。
桥的定义为:对于一条
强制在线。
\texttt{solution}
考虑用并查集维护连通情况。对于边双树,考虑维护有根树,设
如果第
为了缩点,我们要先求出
如果
综上,该算法的总复杂度是
\texttt{part 6} 可撤销并查集
可撤销并查集是一种扩展了标准并查集数据结构的数据结构,它允许在进行
这种数据结构通常用于支持一些需要回滚操作的应用,比如在离线算法中或者需要进行回退的决策问题中。
当然主要是应用于在线算法,因为离线算法反悔可以完全输入后在一起处理。
你需要写一个数据结构实现以下三种操作
1: 连接 x,y 。
2: 询问 x 与 y 是否连通 。
3: 撤回操作。
因为会破坏树形结构,导致在撤销操作时无法准确找到原始的父节点,不可以使用路径压缩来优化并查集。
当然,启发式合并或者按秩合并这两种合并方式对于并查集来说是没有用影响的。
因此,用栈来记录每次合并的操作,然后进行对
算法原理:
- 查找
由于支持撤销操作,那么肯定不能路径压缩,否则会破坏树形结构,反悔时无法找到原本的父节点。
- 合并
既然不能路径压缩,我们要尽可能地减少树的深度。于是启发式合并 (或按秩合并)。
- 撤销
用栈来记录每次合并的操作。
用一个
用
stack<pair<int*,int>> st;
int fa[],rnk[];
void init(int n){
for(int i=0;i<=n;i++){
fa[i]=i;
rnk[i]=0;
}
}
int find(int x){
if(x==fa[x]){
return x;
}
return find(fa[x]);
}
void uni(int x,int y){
x=find(x);
y=find(y);
if(x==y){
return;
}
if(rnk[x]<=rnk[y]){
st.push({fa+x,fa[x]});
fa[x]=y;
if(rnk[x]==rnk[y]){
st.push({rnk+y,rnk[y]});
rnk[y]++;
}
}
else{
st.push({fa+y,fa[y]});
fa[y]=x;
}
}
void undo(){
*stk.top().first=st.top().second;
stk.pop();
}
\texttt{part 7} 可删除并查集
你需要写一个数据结构实现以下三种操作
1: 合并 x,y 所在的集合。
2: 把 x 移到 y 所在的集合。
3: 求 x 所在集合元素个数与元素和。
我们并不可以通过
因为如果我们直接
fa[find(x)]=find(y)
的话,
我们可以通过给每个元素设置一个虚拟父节点,每次移动的时候操作的是实际的节点,而不是父节点即可完成。
因为这个时候操作的节点都是叶子节点,不会有副作用。
如果
换言之只要每个集合的根节点不可能作为
但是题目中
方便起见我们把这
\texttt{part 8} 可持久化并查集
引入
你需要写一个数据结构实现以下三种操作
1: 合并 x,y 所在的集合。
2: 回到第 k 次操作(执行三种操作中的任意一种都记为一次操作)之后的状态;。
3: 询问 x 与 y 是否在同一个集合内。
可持久化并查集是建立在可持久化数组上的,在学习可持久化并查集之前,需要先学习可持久化权值线段树,权值线段树。
众所周知,并查集有两种优化方式:
-
路径压缩
-
按秩合并
我们的并查集的
return fa[x]==x?x:fa[x]=find(fa[x]);
每递归一次,只要进入
对于建立在主席树上的可持久化数组,每进行一次单点修改就会多一个新的版本存放新的结点。
会导致
因为每个版本的并查集的结点深度可能是不一样的,所以我们还需要要新开一个可持久化数组来记录每个版本的
算法内容
对于操作
对于操作
#include<bits/stdc++.h>
using namespace std;
inline long long read(){
long long x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9') {
x=(x<<1)+(x<<3)+ch-'0';
ch=getchar();
}
return x*f;
}
void write(long long x){
if(x<0){
putchar('-');
x=-x;
}
if(x>9)
write(x/10);
putchar(x%10+'0');
return;
}
int n,m,root[1000001],dfn,cnt;
struct union_set{
struct node{
int lson,rson,fa,dep;
}t[8000001];
int build(int l,int r){
int x=++cnt;
if(l==r){
t[x].fa=l;
return x;
}
int mid=(l+r)>>1;
t[x].lson=build(l,mid);
t[x].rson=build(mid+1,r);
return x;
}
int update(int id,int l,int r,int x){
if(l==r){
return id;
}
int mid=(l+r)>>1;
if(x<=mid){
return update(t[id].lson,l,mid,x);
}
else{
return update(t[id].rson,mid+1,r,x);
}
}
int find(int x,int pre){
int fa=update(root[x],1,n,pre);
if(t[fa].fa==pre){
return fa;
}
return find(x,t[fa].fa);
}
int build(int x){
int id=++cnt;
t[id]=t[x];
return id;
}
int uni(int id,int l,int r,int x,int fa){
int to=build(id);
if(l==r){
t[to].fa=fa;
return to;
}
int mid=(l+r)>>1;
if(x<=mid){
t[to].lson=uni(t[id].lson,l,mid,x,fa);
}
else{
t[to].rson=uni(t[id].rson,mid+1,r,x,fa);
}
return to;
}
int add(int id,int l,int r,int x){
int to=build(id);
if(l==r){
t[to].dep++;
return to;
}
int mid=(l+r)>>1;
if(x<=mid){
t[to].lson=add(t[id].lson,l,mid,x);
}
else{
t[to].rson=add(t[id].rson,mid+1,r,x);
}
return to;
}
void merge(int x,int l,int r){
root[x]=root[x-1];
l=find(x,l);
r=find(x,r);
if(t[l].fa!=t[r].fa){
if(t[l].dep>=t[r].dep){
swap(l,r);
}
root[x]=uni(root[x-1],1,n,t[l].fa,t[r].fa);
if(t[l].dep==t[r].dep){
root[x]=add(root[x],1,n,t[r].fa);
}
}
}
int same(int x,int l,int r){
l=find(x,l);
r=find(x,r);
if(t[l].fa==t[r].fa){
return 1;
}
else{
return 0;
}
}
}tree;
int op,x,y;
signed main(){
n=read();
m=read();
root[0]=tree.build(1,n);
for(int i=1;i<=m;i++){
op=read();
if(op==1){
x=read();
y=read();
tree.merge(i,x,y);
}
if(op==2){
x=read();
root[i]=root[x];
}
if(op==3){
x=read();
y=read();
write(tree.same(i-1,x,y));
printf("\n");
root[i]=root[i-1];
}
}
return 0;
}