并查集基础知识:
功能:实现集合的合并与查找
实现原理:
- 用树表示集合,若根节点相同则属于同一个集合,否则不属于同一个集合
- 合并:将一个集合的根节点连接在另一个根节点的下面
按秩合并:
秩:树的高度
即将矮树的连接在高树的下面,若高度相同,则任意连接,并将树高加一。
int union_root(int x,int y,int r[])
{
int x_root=find_root(x);
int y_root=find_root(y);
if(x_root==y_root)//如果在同一集合,即根节点相同
{
return 0;
}
else//如果不在同一集合,即根节点不同,则将矮树的连接在高树的下面
{
if(r[x_root]>r[y_root])
{
fa[y_root]=x_root;
}
else if(r[x_root]<r[y_root])
{
fa[x_root]=y_root;
}
else//若高度相同,则任意连接,并将树高加一
{
fa[x_root]=y_root;
r[y_root]++;
}
return 1;
}
}
路径压缩:
在搜索树时,若节点的父节点不是根结点,则将树的父节点直接改为根结点,减少下次搜索时间
int find_root(int x)
{
if(fa[x]==x)return x;//若父节点是根结点
fa[x]=find_root(fa[x]);//若节点的父节点不是根结点,则将树的父节点直接改为根结点
return fa[x];
}
简写版:
int find_root(int x)
{
return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}
一般来说并查集用路径压缩比较多,因为不用计算树高,写起来更简单。只用按秩合并或路径压缩的时间复杂度都是O(logn),按秩合并+路径压缩的并查集单次操作可以看作O(n)
相关算法学习视频:
练习:
不带权的并查集:
洛谷P3367 【模板】并查集
#include <bits/stdc++.h>
using namespace std;
int n,m;
int fa[10005];
int find_root(int x)//路径压缩
{
return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}
int judge_root(int x,int y,int z)
{
int x_root=find_root(x);
int y_root=find_root(y);
if(z==1)
{
fa[x_root]=y_root;
}
if(z==2)
{
if(x_root==y_root)
{
printf("Y\n");
}
else
{
printf("N\n");
}
}
return 0;
}
void init()
{
for(int i=1;i<=n;i++)
{
fa[i]=i;
}
}
int main()
{
int z,x,y;
while(cin>>n>>m)
{
init();
while(cin>>z>>x>>y)
{
judge_root(x,y,z);
}
}
return 0;
}
畅通工程并查集版
#include <bits/stdc++.h>
using namespace std;
int n,m,ans;
int fa[10005];
int find_root(int x)//路径压缩
{
return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}
int union_root(int x,int y)//合并
{
int x_root=find_root(x);
int y_root=find_root(y);
if(x_root!=y_root)
{
fa[x_root]=y_root;
ans--;
}
return 0;
}
void init()//初始化
{
for(int i=1;i<=n;i++)
{
fa[i]=i;
}
}
int main()
{
int x,y;
while(cin>>n>>m&&n!=0)
{
ans=n-1;
init();
while(m--)
{
scanf("%d%d",&x,&y);
union_root(x,y);
}
printf("%d\n",ans--);
}
return 0;
}
小希的迷宫
这个题要注意如果最后结果是多个联通分支是不满足题目条件“任意两个房间有且仅有一条路径可以相通”的,需要特判一下。
#include <bits/stdc++.h>
using namespace std;
int n,m;
int fa[100005],vis[100005];
struct sa
{
int x,y;
}a[100005];
int find_root(int x)
{
return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}
int union_root(int x,int y)
{
int x_root=find_root(x);
int y_root=find_root(y);
if(x_root!=y_root)
{
fa[x_root]=y_root;
return 0;
}
else{return 1;}
}
int main()
{
int x,y,f,cnt;
while(1)
{
n=0;
f=0;
memset(fa,0,sizeof(fa));
memset(vis,0,sizeof(vis));
while(cin>>x>>y)
{
if(x==-1&&y==-1){break;}
if(x==0&&y==0){break;}
a[n].x=x;
a[n].y=y;
fa[a[n].x]=a[n].x;
fa[a[n].y]=a[n].y;
vis[a[n].x]=1;
vis[a[n].y]=1;
n++;
}
for(int i=0;i<n;i++)
{
if(union_root(a[i].x,a[i].y)==1){f=1;}
}
cnt=0;
for(int i=0;i<100005;i++)//判断是否存在多个联通分支
{
if(vis[i]&&fa[i]==i)
{
cnt++;
}
}
if(cnt>1){f=1;}
if(!(x==-1&&y==-1))
{
if(f==1)
{
printf("No\n");
}
else
{
printf("Yes\n");
}
}
if(x==-1&&y==-1){break;}
}
return 0;
}
带权的并查集:
带权的并查集与不带权的并查集相比多了一个比较的过程,实现原理是一样的。
P3366 【模板】最小生成树
最小生成树有两个算法:Prim和Kruskal,我写的都是Kruskal算法。
算法原理:最小生成树(Kruskal(克鲁斯卡尔)和Prim(普里姆))算法动画演示
#include <bits/stdc++.h>
using namespace std;
const int MAXN=200005;
int n,m;
int fa[MAXN],vis[MAXN];
struct sa
{
int x,y,z;
}edge[MAXN];
int find_root(int x)
{
return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}
bool cmp(struct sa x,struct sa y)
{
return x.z<y.z;
}
int main()
{
while(cin>>n>>m)
{
for(int i=1;i<=m;i++)
{
cin>>edge[i].x>>edge[i].y>>edge[i].z;
}
sort(edge+1,edge+1+m,cmp);
for(int i=1;i<=n;i++)
{
fa[i]=i;
vis[i]=1;
}
int sum=0,total=0;
for(int i=1;i<=m;i++)
{
if(total==n-1){break;}
int u=edge[i].x;
int v=edge[i].y;
int u_root=find_root(u);
int v_root=find_root(v);
if(u_root!=v_root)
{
fa[u_root]=v_root;
sum+=edge[i].z;
total++;
}
}
int cnt=0;
for(int i=1;i<=n;i++)
{
if(fa[i]==i)
cnt++;
}
//printf("cnt=%d\n",cnt);
if(cnt>1){printf("orz\n");}
else{printf("%d\n",sum);}
}
return 0;
}
湖南修路
这题要考虑已经修过的路无论权值多少都不用再修的情况。
#include <bits/stdc++.h>
using namespace std;
const int MAXN=200005;
int n,m;
int fa[MAXN],vis[MAXN];
struct sa
{
int x,y,z;
int flag;
}edge[MAXN];
int find_root(int x)
{
return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}
bool cmp(struct sa x,struct sa y)
{
if(x.flag!=y.flag)//若路已经修过了,则不用考虑其权值都排在前面,若没修过,则按权值从小到大排
{
return x.flag>y.flag;
}
else
return x.z<y.z;
}
int main()
{
while(cin>>n)
{
if(n==0){break;}
m=(n*(n-1))/2;
for(int i=1;i<=m;i++)
{
cin>>edge[i].x>>edge[i].y>>edge[i].z>>edge[i].flag;
}
sort(edge+1,edge+1+m,cmp);
for(int i=1;i<=n;i++)
{
fa[i]=i;
}
int total=0,sum=0;
for(int i=1;i<=m;i++)
{
if(total==n-1){break;}
int u=edge[i].x;
int v=edge[i].y;
int u_root=find_root(u);
int v_root=find_root(v);
if(u_root!=v_root)
{
if(!edge[i].flag)
{
fa[u_root]=v_root;
sum+=edge[i].z;
}
total++;
}
}
printf("%d\n",sum);
}
return 0;
}
最小树1
#include <bits/stdc++.h>
using namespace std;
const int MAXN=10005;
int n,m;
int fa[MAXN],vis[MAXN];
struct sa
{
int x,y,z;
}edge[MAXN];
int find_root(int x)
{
return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}
bool cmp(struct sa x,struct sa y)
{
return x.z<y.z;
}
int main()
{
while(cin>>m>>n)
{
if(m==0){break;}
for(int i=1;i<=m;i++)
{
cin>>edge[i].x>>edge[i].y>>edge[i].z;
}
sort(edge+1,edge+1+m,cmp);
for(int i=1;i<=n;i++)
{
fa[i]=i;
vis[i]=1;
}
int sum=0,total=0;
for(int i=1;i<=m;i++)
{
if(total==n-1){break;}
int u=edge[i].x;
int v=edge[i].y;
int u_root=find_root(u);
int v_root=find_root(v);
if(u_root!=v_root)
{
fa[u_root]=v_root;
sum+=edge[i].z;
total++;
}
}
int cnt=0;
for(int i=1;i<=n;i++)//检查联通分支数
{
if(fa[i]==i)
cnt++;
}
if(cnt>1){printf("?\n");}//若联通分支数>1,则不是最小生成树
else{printf("%d\n",sum);}
}
return 0;
}
剩下几个题太相似了,就不贴了。
最小瓶颈路
定义:
最小瓶颈路即为两点之间所有路径中权值最大的边最小的路径
案例分析:
案例所示图如下
以1 7为例:
在众多的路径当中,显然图示这条路径的边的最大权值最小,为30。
结合Kruskal算法我们可以这样思考:在生成最小生成树的同时,检查两个点是否在同一个连通分支里,当添加某一边使得恰好两个点处于同一连通分支中时,该边即为所求。
关于最小瓶颈路的讲解还可以参考:
最小瓶颈路含义
20190521测试总结
#include <bits/stdc++.h>
using namespace std;
const int MAXN=100005;
int n,m,k;
int fa[MAXN],ans[MAXN];//fa[MAXN]保存父节点,ans[MAXN]保存询问的答案
struct sa
{
int x,y,z;
}edge[MAXN],vis[MAXN];
int find_root(int x)
{
return (fa[x]==x)?x:fa[x]=find_root(fa[x]);
}
bool cmp(struct sa x,struct sa y)
{
return x.z<y.z;
}
int main()
{
while(~scanf("%d%d%d",&n,&m,&k))
{
memset(ans,-1,sizeof(ans));
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&edge[i].x,&edge[i].y,&edge[i].z);
}
sort(edge+1,edge+1+m,cmp);
for(int i=1;i<=n;i++)
{
fa[i]=i;
}
for(int i=1;i<=k;i++)
{
scanf("%d%d",&vis[i].x,&vis[i].y);
}
int total=0;
for(int i=1;i<=m;i++)
{
if(total==n-1){break;}
int u=edge[i].x;
int v=edge[i].y;
int u_root=find_root(u);
int v_root=find_root(v);
if(u_root!=v_root)
{
fa[u_root]=v_root;
total++;
for(int j=1;j<=k;j++)
{
if(ans[j]==-1&&find_root(vis[j].x)==find_root(vis[j].y))//检查两个点是否在同一个连通分支里,当添加某一边使得恰好两个点处于同一连通分支中时,该边即为所求
{
ans[j]=edge[i].z;
}
}
}
}
for(int i=1;i<=k;i++)
{
printf("%d\n",ans[i]);
}
}
return 0;
}