所谓并查集,实际上可以认为是对集合的合并与查找,所以下面从集合开始讲解。
假设有集合A:1 2 3 4 5,集合B:6 7 8 9 0;假设此时有一个条件导致A中的随便一个数与B中的随便一个数在一个集合内,那么很显然AB将会合并成一个更大的集合。
接下来考虑算法:
对一个数组f[50]来说,先对其进行初始化,即:
for(int i=0;i<50;i++)
f[i]=i;
f[i]的含义为f[i]与i在一个集合内。由于此时经过初始化后,f[i]即为i本身,意思是i与自己在一个集合内。
接下来考虑如何将两个集合合并到同一个集合内。
假设有一个集合A:1 2 3 4 集合B:5 6,那么在计算机中他们应该是以下面的形式表示的(表示形式不唯一,下面只是很多情况中的一种)
图中的含义为f[4]=2,f[2]=1,f[3]=1;f[6]=5,之所以可以这样表示为两个集合,是因为从下往上查找时,同集合的最终结果一定是相同的,所以如果想要将集合B合并到集合A中,只需要让集合B最上面的元素连到集合A中的任意一个元素上即可,如以下图片
这样的话,由于6最考上的数变成了三,与其他的相同,所以A与B就等同于合并成了同一个集合。
下面给出查找的代码
int find(int x)
{
while(x!=f[x])
{
x=f[x];
}
return x;
}
这个是合并的代码
void unionf(int x,int y)
{
int fx=find(x);
int fy=find(y);
if(fx!=fy) //不在同一集合内
{
f[fx]=fy;
}
}
读者应该考虑到,如果在通过调用find()函数时,由于可能因为while循环执行次数过多,导致超时的问题,这种情况下,应该优化查找时的代码,下面给出为什么可以优化以及代码
还用之前的图,假设要调用find()函数,查找6的根节点,此时会在循环中查找5 3 1 的根节点,最后返回1,这也就意味着,不仅仅6的根节点是1,其他过程中的几个的根节点都是1,于是可以在求得根节点后,先不着急输出,而是再次循环一次,把路径中的根节点全部换成1,这样以后再次查找其中某个节点的根结点时,就再也不需要进入循环了。
优化代码如下:
int find(int x)
{
int y=x;
while(x!=f[x])
{
x=f[x];
} //循环结束后x即为根
while(y!=f[y])
{
int z=f[y]; //保存路径的下一个节点
f[y]=x; //更改当前节点的根节点
y=z; //回复被保存的节点
}
return x;
}
经过这样的操作后,原图会变成这种情形
显然下一次再查找时所需要的时间就会大幅度降低了。
其实查找还有递归算法,而且更加简单容易理解一点,相信大家只要理解了上面的内容这个一看就懂,就不再讲述了:
int find(int x)
{
if(x==f[x])return x;
return f[x]=find(f[x]);
}