注意:学习并查集算法前需要有一定的递归思想的基础,如果要更好的理解并查集的话,在看该博客时可以在纸上进行验算。
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。常常在使用中以森林来表示。
并查集的执行具体分为三步:初始化,查找,合并
下面以查找有几个犯罪团伙为例进项算法思想解释:
有n个强盗m条线索,问有多少个犯罪团伙,线索如下:
一、初始化:因为最开始不知道强盗谁与谁是同伙,所以初始化他们的同伙是他们自己。在这里数组的下标表示是几号强盗,而数组里面的数表示它的首领是谁,初始化时因为没有同伙(即没有首领),此时他们的首领就为它们自己。
void init()
{
for(int i=1;i<=n;i++)
f[i]=i;
}
二、查找与合并:根据线索可以知道1号和2号是同伙,所以可以假设他们中间有一个首领即boss,那么他们两个究竟谁当首领呢?在这里我们遵循靠左法则,即每一条线索中我们都规定左边的为首领,那么右边的强盗的首领就为左边的强盗。那么此时就需要将右边强盗里面的数值改为左边强盗里面存放的数值,
此时强盗1和强盗2证明了它们是同一个团伙。
注意:在这里需要注意一点,在下面的线索中出现了5号强盗与2号强盗是同伙,但是由于二号已经有了首领那么此时就要遵循擒贼先擒王的原则,即就是将原先2号强盗的首领1号强盗的首领变成5号首领,那么此时2号强盗的首领也要变成5号强盗。
注意:上述将2号强盗的首领变成5号强盗并不是必须的,因为该步骤寻找首领的操作是采用递归的方式,所以不管2号改不改变结果都是一样的。
最后的图为下图:
那么此时数组中数组下标与数组中的数相等的个数,即为强盗团伙的个数,如下图:
查找合并的具体代码如下:
//这是查找首领的函数,不停的去找直到找到首领位置,也就是擒贼先擒王
int getf(int v)
{
//找到首领了就返回上一级
if(f[v]==v)
return v;
else//没有找到就继续寻找首领
{
/*这里是路径压缩,每次在函数返回的时候,顺便把路上遇到的人的“boss”
改为最后找到的首领编号也就是犯罪团伙的最高领导人编号。
这样可以提高今后找到最高领导人(其实就是树的祖先的)速度*/
f[v]=getf(f[v]);
return f[v];
}
}
//这是将强盗合并的函数,
void merge(int v,int u)
{
int t1=getf(v);
int t2=getf(u);
if(t1!=t2)//判断两个结点是否在同一个集合中,即是否为同一个首领
{
f[t2]=f[t1];
/*“靠左原则”,左边变成右边的boss,即把右边的集合,作为左边的子集合
经过路径压缩后,将f[u]的根的值也赋值为v的祖先f[t1]*/
}
}
并查集完整代码如下:
#include<stdio.h>
int f[1000]={0},n,m,k,sum=0;
//初始化
void init()
{
for(int i=1;i<=n;i++)
f[i]=i;
}
//这是查找首领的函数,不停的去找直到找到首领位置,也就是擒贼先擒王
int getf(int v)
{
//找到首领了就返回上一级
if(f[v]==v)
return v;
else//没有找到就继续寻找首领
{
/*这里是路径压缩,每次在函数返回的时候,顺便把路上遇到的人的“boss”
改为最后找到的首领编号也就是犯罪团伙的最高领导人编号。
这样可以提高今后找到最高领导人(其实就是树的祖先的)速度*/
f[v]=getf(f[v]);
return f[v];
}
}
//这是将强盗合并的函数,
void merge(int v,int u)
{
int t1=getf(v);
int t2=getf(u);
if(t1!=t2)//判断两个结点是否在同一个集合中,即是否为同一个首领
{
f[t2]=f[t1];
/*“靠左原则”,左边变成右边的boss,即把右边的集合,作为左边的子集合
经过路径压缩后,将f[u]的根的值也赋值为v的祖先f[t1]*/
}
}
int main()
{
int x,y;
scanf("%d %d",&n,&m);
//初始化
init();
for(int i=1;i<=m;i++)
{
//合并犯罪团伙
scanf("%d %d",&x,&y);
merge(x,y);
}
for(int i=1;i<=n;i++)
{
//输出看有几个犯罪团伙
if(f[i]==i)
sum++;
}
printf("%d\n",sum);
return 0;
}