zoukankan      html  css  js  c++  java
  • 并查集知识学习

    (转)

    并查集的作用:并和查,即合并和查找,将一些集合合并,快速查找或判断某两个集合的关系,或某元素与集合的关系,或某两个元素的关系。

    并查集的结构:并查集主要操作对象是森林,树的结构赋予它独特的能力,对整个集合操作转换为对根节点(或称该集合的代表元素)的操作,一个集合里的元素关系不一定确定,但相对于根节点的关系很明了,这也是为了查找方便。

    并查集优化方法:按秩合并和路径压缩的配合使用,使得查找过程优化到极致。按秩合并,每次将深度小的树合并到深度大的树里面去,使得整棵树尽量矮;路径压缩,将当前节点到根节点路径上的所有点直接连到根节点上,使得每个点到根节点的距离更短,在下一次查找的时候更快。

     

    如何快速确定偏移量公式:

    例:现在要合并节点x,y, 找到根节点fx = Find(x); fy = Find(y);一般情况下,根节点的偏移量都保持为0, offset[foot] = 0;如果要使得x和y的偏移量为t,假设fx指向fy,则可以写出公式offset[x] + offset[fx] - offset[y] = t,则offset[fx] = (offset[y] + t - offset[x]) % n; 这个n即为总共有多少类,如:在poj1182 食物链中n = 3,,在poj2492 A Bug's Life中n = 2, 这样fx的偏移量就计算出来了,只需要改其中一个根节点的偏移量,这里是fx,因为假设是fx指向fy。

    非递归路劲压缩:

     1 代码
     2 
     3 int Find(int x){
     4     int r = x;
     5     while (r != bin[r]){
     6           r = bin[r];
     7     }
     8     int y = x;
     9     while (y != bin[y]){
    10           y = bin[y];
    11           bin[y] = r;
    12     }
    13     return r;    
    14 }
    View Code

    递归式路径压缩:

    1 int Find(int x){
    2     if (x != bin[x]){
    3        return bin[x] = Find(bin[x]);
    4     }
    5     return x;
    6 }
    View Code

    部分并查集题:

    poj1611 The Suspects  题解

    poj2492 A Bug's Life  题解

    poj1182 食物链  题解

    hdu1558 Segment set   题解

    hdu1198 Farm Irrigation  题解

    三、并查算法

    通过对上面引题的分析,我们已经十分清楚——所谓并查集算法就是对不相交集合(disjoint set)进行如下两种操作:

    (1)检索某元素属于哪个集合;

    (2)合并两个集合。

    我们最常用的数据结构是并查集的森林实现。也就是说,在森林中,每棵树代表一个集合,用树根来标识一个集合。有关树的形态在并查集中并不重要,重要的是每棵树里有那些元素。

    1. 合并操作

    为了把两个集合S1和S2并起来,只需要把S1的根的父亲设置为S2的根(或把S2的根的父亲设置为S1的根)就可以了。

    这里有一个优化:让深度较小的树成为深度较大的树的子树,这样查找的次数就会少些。这个优化称为启发式合并。可以证明:这样做以后树的深度为O(logn)。即:在一个有n个元素的集合,我们将保证移动不超过logn次就可以找到目标。

    【证明】我们合并一个有i个结点的集合和一个有j个结点的集合,我们设i≤j,我们在一个小的集合中增加一个被跟随的指针,但是他们现在在一个数量为i+j的集合中。由于:

    1+log i=log(i+i)<=log(i+j);

    所以我们可以保证性质。

    由于使用启发式合并算法以后树的深度为O(logn),因此我们可以得出如下性质:启发式合并最多移动2logn次指针就可以决定两个事物是否想联系。

    同时我们还可以得出另一个性质:启发式快速合并所得到的集合树,其深度不超过 ,其中n是集合S中的所有子集所含的成员数的总和。

    【证明】我们可以用归纳法证明:

    当i=1时,树中只有一个根节点,即深度为1

    又|log2 1|+1=1所以正确。

    假设i≤n-1时成立,尝试证明i=n时成立。

    不失一般性,可以假设此树是由含有m(1≤m≤n/2)个元素,根为j的树Sj,和含有n-m个元素、根为k的树Sk合并而得到,并且,树j合并到树k,根是k。

    (1)若合并前:子树Sj的深度<子树Sk的深度

    则合并后的树深度和Sk相同,深度不超过:

    |log2(n-m)|+1

    显然不超过|log2 n|+1;

    (2)若合并前:子树Sj的深度≥子树Sk的深度

    则合并后的树的深度为Sj的深度+1,即:

    (|log2m|+1)+1=|log2(2m)|+1<=|log2n|+1 

    小结:实践告诉我们,上面所陈述的性质对于一个m条边n个事物的联系问题,最多执行mlogn次指令。我们只是增加了一点点额外的代码,我们就把程序的效率很大地提升了。大量的实验可以告诉我们,启发式合并可以在线形时间内解答问题。更确切地说,这个算法运行时间的花费,很难再有更加明显的优秀、高效的算法了。

    2. 查找操作

    查找一个元素u也很简单,只需要顺着叶子到根结点的路径找到u所在的根结点,也就是确定了u所在的集合。

    这里又有一个优化:找到u所在树的根v以后,把从u到v的路径上所有点的父亲都设置为v,这样也会减少查找次数。这个优化称作路径压缩(compresses paths)。

    压缩路径可以有很多种方法,这里介绍两种最常用的方法:

    (1)满路径压缩(full compresses paths):这是一种极其简单但又很常用的方法。就是在添加另一个集合的时候,把所有遇到的结点都指向根节点。

    (2)二分压缩路径(compresses paths by halving):具体思想就是把当前的结点,跳过一个指向父亲的父亲,从6而使整个路径减半深度减半。这种办法比满路径压缩要快那么一点点。数据越大,当然区别就会越明显。

    压缩路径的本质使路径深度更加地减小,从而使访问的时候速度增快,是一种很不错的优化。在使用路径压缩以后,由于深度经常性发生变化,因此我们不再使用深度作为合并操作的启发式函数值,而是使用一个新的rank数。刚建立的新集合的rank为0,以后当两个rank相同的树合并时,随便选一棵树作为新根,并把它的rank加1;否则rank大的树作为新根,两棵树的rank均不变。

    3. 时间复杂度

    并查集进行n次查找的时间复杂度是O(n )(执行n-1次合并和m≥n次查找)。其中 是一个增长极其缓慢的函数,它是阿克曼函数(Ackermann Function)的某个反函数。它可以看作是小于5的。所以可以认为并查集的时间复杂度几乎是线性的。

    通过上面的分析,我们可以得出:并查集适用于所有集合的合并与查找的操作,进一步还可以延伸到一些图论中判断两个元素是否属于同一个连通块时的操作。由于使用启发式合并和路径压缩技术,可以讲并查集的时间复杂度近似的看作O(1),空间复杂度是O(N),这样就将一个大规模的问题转变成空间极小、速度极快的简单操作。

    并查集模板

     1 1、make_set(x) 把每一个元素初始化为一个集合
     2 建立一个新的集合,其中集合只有唯一的一个元素x
     3  
     4 2、union_set(x, y) 按秩合并x,y所在的集合
     5   
     6 3、find_set(x)返回x所在的集合的代表 
     7  
     8     
     9    在执行查找操作时,要沿着父节点指针一直找下去,直到找到树根为止。大家要注意途中的箭头。 
    10 4、实现并查集的标准代码: 
    11  1 #include <stdio.h>
    12  2 
    13  3 const int MAXN = 100; /*结点数目上线*/
    14  4 int pa[MAXN];    /*p[x]表示x的父节点*/
    15  5 int rank[MAXN];    /*rank[x]是x的高度的一个上界*/
    16  6 
    17  7 void make_set(int x)
    18  8 {/*创建一个单元集*/
    19  9     pa[x] = x;
    20 10     rank[x] = 0;
    21 11 }
    22 12 
    23 13 int find_set(int x)
    24 14 {/*带路径压缩的查找*/
    25 15     if(x != pa[x])
    26 16         pa[x] = find_set(p[x]);
    27 17     return pa[x];
    28 18 }
    29 19 
    30 20 /*按秩合并x,y所在的集合*/
    31 21 void union_set(int x, int y)
    32 22 {
    33 23     x = find_set(x);
    34 24     y = find_set(y);
    35 25     if(rank[x] > rank[y])/*让rank比较高的作为父结点*/
    36 26         pa[y] = x;
    37 27     else 
    38 28     {
    39 29         pa[x] = y;
    40 30         if(rank[x] == rank[y])
    41 31             rank[y]++;
    42 32     }
    43 33 }
    View Code
    越努力,越幸运
  • 相关阅读:
    [POJ 3253] Fence Repair
    [POJ 1422] Air Raid
    [POJ 2195] Going Home
    [POJ 1273] Drainage Ditches
    [BZOJ 1718] Redundant Paths
    [POJ 1041] John's Trip
    [NOI 2003] 逃学的小孩
    __attribute__((noreturn))的用法
    回味经典——uboot1.1.6 之 第二阶段 第三阶段
    回味经典——uboot1.1.6 之 第一阶段
  • 原文地址:https://www.cnblogs.com/qinduanyinghua/p/5487443.html
Copyright © 2011-2022 走看看