该文仅供初学者初步认识并查集以及并查集的基本操作。
先通过以下问题引入并查集:
有n个盗贼,有m条信息,每条信息表示i盗贼和j盗贼属于同一团伙,询问共有多少团伙。
简化问题,就是有n个点,m条边连接这些点,问最后有多少个独立的团。
每条信息连接的两个盗贼属于同一团伙,设为团伙k,那么之后无论哪个盗贼与这个团伙里的任意盗贼相连,也都是属于团伙k的。我们假设团伙k是有组织有纪律的,每个人都有他的领导,那么我们可以以该团伙的最高领导的编号来当做该团伙k的编号。假设我们已经知道了所有人的领导是谁,那么最后我们只要知道有几个最高领导就知道有多少个团伙了!
那么我们可以进行以下操作:
1、假设每个盗贼都是最高领导(为方便操作,我们把所有盗贼的领导都记为自己编号,那么之后只要知道哪些盗贼的领导编号是自己的编号,就知道哪些盗贼是团伙的最高领导了)
for(int i=1;i<=n;i++) fa[i]=i;(这里的fa数组相当于记录领导编号的数组)
2、对于每一条信息,对于i和j,我们先找到当前i所在团伙的最高领导,再找到j所在团伙的最高领导,分别记为i1,j1。
int find(int x){
while(x!=fa[x]) x=fa[x];
return x;
}
①如果i1==j1 ,说明这两个盗贼所在的团伙的最高领导是同一个人,那么两个盗贼就是在同一个团伙,这条信息就无法提供更多资料,就可以结束对这条信息的分析了。
②如果i1!=j1 , 说明这两个盗贼在两个团伙里,那么我们可以假设j1是i1的领导(因为我们并不在意究竟谁才是最高领导,我们只在意最高领导的数量),那么fa[i1]=j1。 我们就把这两个团伙给合并为一个团伙了。
3、最后我们从1到n循环,看哪几个盗贼是最高领导,就是看哪几个位置的fa[i]==i,就知道有多少个盗贼团伙了!
综上:
初始化:
for(int i=1;i<=n;i++) fa[i]=i;
进行合并操作:(我们把每条信息记为x,y)
int getfather(int x){
if(fa[x]==x) return x;else return getfather(fa[x]);
for(int i=1;i<=m;i++){
scanf("%d%d",&x,&y);
int xx=getfather(x),yy=getfather(y);
if(xx==yy)continue;
fa[xx]=yy;
}
最后查询:
for(int i=1;i<=n;i++) if(i==fa[i]) ans++;
这样这题就能完成了!
但是有一个问题,就是大家可以发现getfather()这个函数效率很低,如果连接是连成一条线的话(比如fa[1]=1;fa[2]=1;fa[3]=2;fa[4]=3......),调用getfather()的效率就是O(n)。我们可以发现,因为我们并不在意各个团伙中的具体的领导关系,甚至只要知道一个最高领导,其他的全都是直接连接到最高领导也是能起到同样的作用的。那么我们就能进行简化:
int getfather(int x){
if(x==fa[x]) return x;return fa[x]=getfather(fa[x]);
}
这一段代码里,我们可以看到 fa[x]=getfather(fa[x]) ,为了方便理解,我们分开来看。
先求出int u=getfather(fa[x]);
然后赋值:fa[x]=u;
最后返回:return fa[x];
这样就压缩了我们的连接路径。
getfather(fa[x]) 这样求出的就是最高领导的编号(设为p),所以u=p。所以fa[x]=p。这样对于x这个盗贼,我们直接让他位于最高领导的领导下,那么下次我们通过x这个点找到这个团伙的最高领导时,只要O(1)的时间了,这样就大大降低了复杂度。用这种方式优化,可以使得平摊的复杂度为O(1)。
#include
using namespace std;
int n,m,fa[1005],ans;
int gf(int x){
if(fa[x]==x)return x;return fa[x]=gf(fa[x]);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
int xx=gf(x),yy=gf(y);
if(xx==yy) continue;
fa[xx]=yy;
}
for(int i=1;i<=n;i++) if(fa[i]==i) ans++;
printf("%d\n",ans);
}
这就是并查集,有合并操作,就是fa[xx]=yy;有查询操作 gf(int x),来查询祖先。
可以查询有多少个团。我们为每个点加上num属性的话,也可以计算每个团中的点的个数。
给出几道练手的习题:
hdu1232
poj1611
hdu1213