逝者如斯,不舍昼夜

尘世中一个迷途小书童,读书太少,想得太多
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

并查集——HDOJ-1232-畅通工程

Posted on 2016-05-18 20:08  SteveWang  阅读(601)  评论(0编辑  收藏  举报

 

  并查集

 

  并查集(Union-Find Sets)是一种非常精巧而实用的集合,集合中的每个元素仍是一个集合,即它是集合的集合。在并查集中的元素(集合)内部进行查找操作以及并查集中的元素(集合)之间的合并操作的时间复杂度均可视为O(1),它主要用于处理一些不相交集合的合并问题,合并之前,需要先判断两个元素是否属于同一集合,这就需要用查找操作来实现,即先查找后合并

  并查集的原理也比较简单,逻辑上使用树来表示集合,树的每个节点就表示集合中的一个元素,指针指向其直接父节点,根结点对应的元素就是该集合的“代表”,指针指向自己,沿着每个节点的指针不断向上查找,最终就可以找到该树的根节点,即该集合的代表元素。如下图所示。

 

                    

 

  上图有两个集合,其中第一个集合为 {a,b,c,d},代表元素是 a;第二个集合为 {e,f,g},代表元素是 e,它们整体是一个并查集 { {a,b,c,d},  {e,f,g} }。

  假设使用一个足够长的数组来存储集合元素,并查集在初始化时构造出下图的森林,其中每个元素都是一个单元素集合,即父节点是其自身:

 

             

 

  并查集内的合并操作非常简单,就是将一个集合的树根指向另一个集合的树根。这里可以应用一个简单的启发式策略——按秩合并。该方法使用秩来表示树高度的上界,在合并时,总是将具有较小秩的树根指向具有较大秩的树根。简单的说,就是总是将较矮的树作为子树,添加到较高的树中。如图所示,第一个集合为 {a,b,c,d},代表元素是 a,秩为3;第二个集合为 {e,f},代表元素是 e,秩为2;合并两个集合得到集合 {a,b,c,d,e,f},代表元素是 a,秩为3。

 

         

 

  对于并查集内的查找操作,其目的就是找到所在集合的代表元素,如果每次都沿着父节点向上查找,那时间复杂度就是树的高度,完全不可能达到常数级。这里需要应用另一种简单有效的启发式策略——路径压缩。在每次查找时,令查找路径上的每个节点都直接指向我们要找的根节点,如下图所示。

 

         

  

  

   关于并查集,一些常见的用途有求连通子图(判断连通性)、求最小生成树的 Kruskal 算法和求最近公共祖先(Least Common Ancestors, LCA)等。

 

 

  习题:畅通工程  

 

  题目来源:HDOJ-1232-畅通工程

  首先在地图上给你若干个城镇,这些城镇都可以看作点,然后告诉你哪些对城镇之间是有道路直接相连的。最后要解决的是整幅图的连通性问题。比如随意给你两个点,让你判断它们是否连通,或者问你整幅图一共有几个连通分支,也就是被分成了几个相互独立的连通子图。像畅通工程这题,问还需要修几条路,实质就是求有几个连通分支。如果有1个连通分支,说明整幅图上的点都连起来了,不需要再修路了;如果有2个连通分支,则只需要再修1条路,从两个连通分支中各选一个点,把它们连起来,那么所有的点都连起来了;如果有3个连通分支,则只需要再修两条路……

 

  源码如下:

#include<iostream>
using namespace std;

const int CMAX = 1001;    // 最多1000个城镇(编号从1开始)

int parent[CMAX];         // 存放各结点的直接父节点,对于根结点,parent指向本身
int Rank[CMAX];           // 存放各结点的秩,即该结点在子树中的高度(实际上只有根结点的秩在按秩合并时才有价值)

int Find(int x)           // 带有路径压缩的查找过程,返回根结点(代表元素)
{
    if (x != parent[x])
        parent[x] = Find(parent[x]);// 沿着查找路径递归向上直到找到根
    return parent[x];               // 找到根,开始回溯,进行路径压缩
}

void Union(int x, int y)    // 按秩合并两个树(集合),让具有较小秩的根指向具有较大秩的根
{
    int root1 = Find(x);
    int root2 = Find(y);
    if (root1 == root2)     // 根结点相同,无需合并
        return;
    if (Rank[root1] < Rank[root2])
    {
        parent[root1] = root2;
    }
    else
    {
        parent[root2] = root1;
        if (Rank[root1] == Rank[root2]) // 只有秩相等时需要递增根的秩(大树合并小树,根秩不变)
            Rank[root1]++;
    }
}

int main()
{
    int N, M;                              // 城镇数目和道路数目
    while (scanf("%d%d", &N, &M) && N)     // 当N为0时,输入结束
    {
        int tree_num = 0;                  // 树的个数,即连通分支数
        for (int i = 1; i <= N; i++)       // 初始化每个结点独自成树,秩为1
        {
            parent[i] = i;
            Rank[i] = 1;
        }
        int city1, city2;
        for (int i = 1; i <= M; i++)       // 读取M条道路,合并连通的城镇
        {
            scanf("%d%d", &city1, &city2);
            Union(city1, city2);
        }
        for (int i = 1; i <= N; i++)
        {
            if (parent[i] == i)          // 每找到一个根就有一个连通分支
                tree_num++;
        }
        printf("%d\n", tree_num - 1);    // 把这些连通分支连起来需要修tree_num-1条路
    }
    return 0;
}

 

 

  提交结果:

 

 

  参考资料:   《算法导论第3版》—— 21.3 不相交集合森林

          http://blog.csdn.net/dellaserss/article/details/7724401

          http://www.cnblogs.com/cyjb/p/UnionFindSets.html