您的位置:首页 > 其它

不相交集合-并查集

2015-07-18 17:27 197 查看

并查集的定义

不相交集合维护了一个不相交动态集的集合。我们用一个代表来标示每个集合,而这个代表是这个集合的某个成员。

该集合中最主要的两个操作Union(合并)与Find(查询),因此该数据结构也叫做并查集。让我们看一下百度百科的介绍:

在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。——百度百科

可以看出我们只是想要查找某个元素所在的集合或者判断两个元素是否是属于同一个集合,并没有必要把查找的路径返回,这也正是并查集的特点。

并查集的操作

根据并查集的特点,可以得到并查集的基本操作.

[thead]
[/thead]
操作说明
Make-Set(x)建立一个新的集合,它的唯一成员是x
Union(x,y)将包含x和y的两个动态集合合并为一个新的集合
Find-Set(x)返回一个指针,这个指针指向包含x的集合的代表
在一个不相交集较快的实现方式中,我们使用有根树来表示集合,树中每个节点包含一个成员,每棵树代表一个森林,从而多个集合构成了一个不相交集合森林。数中每个节点仅指向其父节点,如下图(图来自算法导论)。



对于这样的实现方式,每个操作的具体流程如下。

Make-Set

对于集合的存储,我们可以选择最简单的数组,数组存储每个元素的父节点。一开始创建没个小集合时,该集合的父节点就是其本身。

int group[MAXN];
void make_set(int x)  
{   /*创建一个单元集*/  
     group[x] = x;  
}


Union

上述图中的最右边的树就是左边两颗树合并之后的结果。这种情况下Union的操作是非常简单的。

void Union(int x,int y)
{
    group[find(x)] = find(y);
}


合并两个节点只需将一个节点的根节点的父亲设定为另外一个节点的根节点即可。

Find-Set

查找某个元素所属的集合,即是查找该元素所在树的根节点,因此最后经过若干次查找,一个节点总是能够找到它的根节点,即满足group[root] = root的节点也就是树的根节点了。

int find(int x)  
{   
    // 寻找x节点所在组的根节点,根节点具有性质group[root] = root  
    while (x != group[x]) x = group[x];  
    return x;  
}


按秩合并与路径压缩

按秩合并

在进行Union时,若能将较少节点的树根指向节点较多的树根,而不是反过来,这样能够让树看起来更加的平衡,并且更加有利于find操作。因此我们拿出额外的空间来存储每个节点的秩,这个秩表示了该节点高度的一个上届,Union操作时,可以让较小高度的秩指向较大高度的秩。

路径压缩

不断地Union之后,有可能出现树的高度很高的极端情况,这样Find操作的效率较低,使用路径压缩可以使查找路径的每个节点直接指向根。如下图:




b图是指向了Find-Set之后的的同一个集合,现在查找路径上每个节点都指向了根。

那么加入按秩合并和路径压缩之后的代码如下:

int group[MAXN];    
int rank[MAXN];    /*rank[x]是x的高度的一个上界*/

void make_set(int x)
{   /*创建一个单元集*/
    group[x] = x;
    rank[x] = 0;
}

int find_set(int x)
{   /*带路径压缩的查找*/
    if(x != group[x])
        group[x] = find_set(group[x]);
    return group[x];
}

/*按秩合并x,y所在的集合*/
void union_set(int x, int y)
{
    x = find_set(x);
    y = find_set(y);
    if(rank[x] > rank[y])/*让rank比较高的作为父结点*/
        group[y] = x;
    else 
    {
        group[x] = y;
        if(rank[x] == rank[y])
            rank[y]++;
    }
}


上述的Find-Set值得一提,其是一种两趟方法,递归时沿着路径找到根节点,回溯时,更新每个节点,使其指向根。

时间复杂度

使用了路径压缩和按秩合并之后,整个并查集的时间复杂能与总操作数m呈线性关系。最坏情况是O(mx(n)),x(n)是一个增长非常慢的函数。具体的证明请参考算法导论21.4节,过程并不简单,这里不再论述。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: