首页 > 技术文章 > 数据结构:并查集(Union Find)

linfangnan 2020-04-06 15:31 原文

导言

我们讲个故事吧,赛特是在欧洲梅罗文加王朝高卢出生的东方人与日耳曼混血儿,深受丕平三世的器重,主要担任收集情报等工作,因为屡立大功而被受封为骑士。因接受丕平三世的任务而秘密离开高卢前往东方寻找所谓的战争不败之术,由此踏上漫漫征途,从威尼斯经地中海到大马士革再到叙利亚,最后经西域进入大唐长安,一去十年。

在路途中遇到了妮可、卡玛、李婧,四人结为好友、邀为同道(二号位有请,哈哈),就以赛特为领队组了个队,然后把大魔王揍了一顿,把反曼陀罗阵拆了。


有一天赛特碰到了陈靖仇,“你也是主角啊!”,“好巧啊,要不我们义结金兰?”于是这两个小队就结盟了。


五湖之内皆兄弟,四海之内皆朋友,陈靖仇等人和古月圣一行人关系不错,他们就成为朋友啦。

当然他们和岳掌门一伙人就没啥关系喽(人人都叫你君子剑,原来你不是真君子啊!)。

并查集

现在我告诉你,上述的内容就是并查集。并查集(Disjoint Set)是一种可以动态维护若干个不重叠的集合,并支持合并与查询两种操作的一种数据结构。按照一般的思路,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中,这样时间复杂度就太高啦。而并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。
并查集支持查找一个元素所属的集合以及两个元素各自所属的集合的合并等运算,当给出两个元素的一个无序对(a,b)时,需要快速“合并” a 和 b 分别所在的集合,这期间需要反复“查找”某元素所在的集合。“并”、“查”和“集” 3 个字由此而来。在这种数据类型中,n个不同的元素被分为若干组。每组是一个集合,这种集合叫分离集合,称之为并查集。

初始化

在赛特、妮可、卡玛、李靖组队之前,他们可以看做是 4 个小队,每个小队只有一个人,领队是他们本身。

例如如有编号为 1, 2, 3, ..., n 的 n 个元素,我们用一个顺序表来组织数据,每一个小队都抽象成一个树结构,使用双亲表示法,数组存储每个元素的双亲,当找不到队友时,结点的双亲即为它本身。

int UFSTree[MAXSIZE];
inline void init(int n)
{
    for (int i = 1; i <= n; i++)
        UUFSTree[i] = i;
}

查找操作

查询(Find)操作的目的是,查询两个元素是否在同一个集合中。例如李靖是属于赛特小队的,古月圣是属于何然小队的,在一个并查集中,我们关心每一个元素是属于哪个集合的,这样就能描述数据之间的从属关系。那么我们是怎么判定元素是属于一个集合的呢?我们可以在一个集合中选择一个领队,用这个领队来代表这个集合,因此在集合的所有元素中不断访问双亲,要保证最终都能访问到领队。

int Find(UFSTree t[], int x)
{
    int i;

    if (t[x].parent == x)
        return t[x].parent;
    return Find(t, t[x].parent);
}

压缩路径

作为一个用于描述元素属于哪个集合的结构, 实际上我们只关心这个元素的领队是哪个结点,并不关心结点之间的关系。例如我们可以把上面的图改造成这样,描述每个队员的领队是赛特的查询速度就提升了:

再稍加改造,速度就更快了:

因此我们可以在查询操作的时候将访问过的每个点都指向领队结点,执行这样的操作之后我们无论访问的是哪个元素,都能够通过一次访问找到它所在的集合。

int Find(UFSTree t[], int x)
{
    int i;

    if (t[x].parent == x)
        return t[x].parent;
    t[x].parent = Find(t, t[x].parent);    //压缩路径
    return t[x].parent;
}

合并(Union)操作的作用是,把两个不相交的集合合并为一个集合。例如上面的例子,想要将赛特小队和陈靖仇小队合并,只需要让陈靖仇的领队是赛特就可以了,这样陈靖仇小队的成员就默认加入赛特小队了。

当然对于一个小队的创建,也同样是使用了“并”的操作,因为每个元素可以看做只有一个元素,领队是它本身的小队,因此组织一个小队同样也可以用该操作实现。

合并操作的实现,需要先找到两个集合的代表元素,然后将前者的双亲设为后者即可,当然也可以将后者的父节点设为前者。

void Union(UFSTree t[],int x, int y)
{
    int idx1, idx2;

    idx1 = Find(t, x);    //查找 x 元素的领队
    idx2 = Find(t, y);    //查找 y 元素的领队
    if (idx1 != idx2)    //若他们的领队不一样
    {
        t[idx2].parent = idx1;    //设置其中一个领队的双亲是另一个领队
    }
}

带有秩的并查集

对于“并”的操作,我们把成员较少的集合往成员较多的集合上合并是比较好的做法,因为这样到节点距离变长的结点个数比较少。因此我们用结构体多开一个成员 rank 表示“秩”,用于记录每个根节点对应的树的深度,如果不为领队,rank 相当于以它作为根结点的子树的深度,因此在初始化的时候,每个小队的都只有它本身,因此深度为 1,就需要把所有元素的 rank 设为 1,合并时比较两个根节点,把 rank 较小者往较大者上合并。

结点结构体定义

typedef struct node{
    int data;    //数据域
    int rank;    //结点的秩
    int parent;    //指向双亲的游标
}UFSTree;    //并查集结点

初始化

void MAKE_SET(UFSTree t[])
{
    for(int i = 1; i <= N; i++)
    {
        t[i].data = i;
        t[i].rank = 0;
        t[i].parent = i;    //双亲初始化为其本身
    }
}

查找元素所属集合

int FIND_SET(UFSTree t[],int x)
{            //在 x 所在的子树中查找集合编号
    if(x != t[x].parent)    //双亲不是自己
        return(FIND_SET(t,t[x].parent));    //从双亲向上查找 x
    else
        return(x);    //双亲是其本身,查找成功
}

合并集合

void UNION(UFSTree t[],int x.int y)
{
    x = FIND_SET(t,x);    //查找 x 所在集合树的编号
    y = FIND_SET(t,y);    //查找 y 所在集合树的编号
    if(t[x].rank > t[y].rank)    //y 结点小于 x 结点的秩
    {
        t[y].parent = x;    //将 y 连接到 x 结点上,x 作为 y 的双亲
    }
    else    //x 结点小于 y 结点的秩
    {
        t[x].parent = y;    //将 x 连接到 y 结点上,y 作为 x 的双亲
        if(t[x].rank == t[y].rank)    //x 与 y 结点的秩相同
        {
            t[y].rank++;    //y 结点的秩加 1
        }
    }
}

应用举例

朋友圈

伪代码

代码实现

#include<iostream>
using namespace std;
#define MAXSIZE 30001
typedef struct node {
    int parent;    //指向双亲的游标
    int length;    //表示朋友圈的长度
}UFSTree;
int Find(UFSTree t[], int x);
void Union(UFSTree t[], int x, int y);

int main()
{
    int M, N, idx, first, count;
    UFSTree t[MAXSIZE];
    int i, j;
    int max = 0;

    cin >> M >> N;
    for ( i = 1; i <= M; i++)    //初始化,注意是 1-M,不要弄错了
    {
        t[i].length = 0;
        t[i].parent = i;
    }
    for ( i = 0; i < N; i++)
    {
        cin >> count >> first;    //输入俱乐部人数和第一个人
        for ( j = 1; j < count; j++)    //注意少处理一人
        {
            cin >> idx;
            Union(t, first, idx);    //将第一个人与后续人合并到一个集合
        }
    }
    for ( i = 1; i <= M; i++)
    {
        idx = Find(t, i);
        t[idx].length++;
        if (t[idx].length > max)
        {
            max = t[idx].length;    //在线处理
        }
    }
    /*for ( i = 1; i < M; i++)
    {
        if (t[i].length > max)
        {
            max = t[i].length;
        }
    }*/
    cout << max;
    return 0;
}

int Find(UFSTree t[], int x)
{
    int i;

    if (t[x].parent == x)
        return t[x].parent;
    t[x].parent = Find(t, t[x].parent);    //压缩路径
    return t[x].parent;
}

void Union(UFSTree t[],int x, int y)
{
    int idx1, idx2;

    idx1 = Find(t, x);
    idx2 = Find(t, y);
    if (idx1 != idx2)
    {
        t[idx2].parent = idx1;
    }
}

参考资料

《数据结构教程》—— 李春葆 主编,清华大学出版社
超有爱的并查集~
算法总结 并查集
算法学习笔记(1) : 并查集

推荐阅读