并查集综述(转载) &&模板

小小的得意一下,本人做的第一个并查集完全是自己想象出来的方法,用后感觉效率不错。后来准备认真学习“标准的”并查集时,发现就是我原来自创的那个方法。

并查集,就是Union-Find Set,也称不相交集合 (Disjoint Set)。这在数据结构中本是很简单的一种东西。当然,我是指算法的实现简单,而非其理论分析简单。但我今天还是要扯一下这个,因为我突然发现我这几天写的并查集都是错的,今天做题时才发现。所以还是把这个很简单的数据结构总结一下。

正如它的名字揭示的,并查集的基本操作有两个:Union和Find。其中Find(x)返回元素x所在的集合的编号,Union(i,j)将i与j所在的集合合并,也就是说在此之后对它们中的每一个调用Find的返回值必须是一样的。

实现时采用父亲表示法的树结构,保证所有在一个集合里的元素都在一棵树内,集合的代表元素就是根。初始时所有节点的父节点都是自身,查找时只须沿着向上的路径查找即可;如果要将两棵树合并只须将其中一棵的根的父节点设为另一棵树的根即可。最主要的优化是所谓路径压缩。简言之,就是在Find的实现时并不简单的return find(U[x]); 而是 return U[x]=find(U[x]); 这样就将当前节点到根的路径上的所有节点的父亲都设为了根,将原本可能比较长的路径“压缩”了。当然这是递归的实现,非递归的话需要沿路径走两次,第一次找到根,第二次再改变。

union操作更简单,只是U[find(x)]=find(y);一句而已。我前两天写Tarjan算法时把这一句错写成U[x]=find(y),竟然也AC了……汗……不过今天做另一道需要并查集的题目就没那么幸运了。

路径压缩的并查集n个操作的序列的均摊复杂度为O(nα(n)),其中 α是阿柯曼函数的一个反函数,增长极慢,在可以想象的n的范围内不超过5,故可视为常数。

并查集的一般用途就是用来维护某种具有自反、对称、传递性质的关系的等价类。

如果:给出各个元素之间的联系,要求将这些元素分成几个集合,每个集合中的元素直接或间接有联系。在这类问题中主要涉及的是对集合的合并和查找,因此将这种集合称为并查集。
链表被普通用来计算并查集.表中的每个元素设两个指针:一个指向同一集合中的下一个元素;另一个指向表首元素。

链结构的并查集

采用链式存储结构,在进行集合查找时的算法复杂度仅为O(1);但合并集合时的算法复杂度却达到了O(n)。如果我们希望两种基本操作的时间效率都比较高的话,链式存储方式就“力不从心”了。

树结构的并查集

采用树结构支持并查集的计算能够满足我们的要求。并查集与一般的树结构不同,每个顶点纪录的不是它的子结点,而是将它的父结点记录下来。下面是树结构的并查集的两种运算方式

⑴直接在树中查询
⑵边查询边“路径压缩”

对应与前面的链式存储结构,树状结构的优势非常明显:编程复杂度低;时间效率高。

直接在树中查询

集合的合并算法很简单,只要将两棵树的根结点相连即可,这步操作只要O(1)时间复杂度。算法的时间效率取决于集合查找的快慢。而集合的查找效率与树的深度呈线性关系。因此直接查询所需要的时间复杂度平均为O(logN)。但在最坏情况下,树退化成为一条链,使得每一次查询的算法复杂度为O(N)。

边查询边“路径压缩

其实,我们还能将集合查找的算法复杂度进一步降低:采用“路径压缩”算法。它的想法很简单:在集合的查找过程中顺便将树的深度降低。采用路径压缩后,每一次查询所用的时间复杂度为增长极为缓慢的ackerman函数的反函数——α(x)。对于可以想象到的n,α(n)都是在5之内的。

并查集:(union-find sets)是一种简单的用途广泛的集合. 并查集是若干个不相交集合,能够实现较快的合并和判断元素所在集合的操作,应用很多。一般采取树形结构来存储并查集,并利用一个rank数组来存储集合的深度下界,在查找操作时进行路径压缩使后续的查找操作加速。这样优化实现的并查集,空间复杂度为O(N),建立一个集合的时间复杂度为O(1),N次合并M查找的时间复杂度为O(M Alpha(N)),这里Alpha是Ackerman函数的某个反函数,在很大的范围内(人类目前观测到的宇宙范围估算有10的80次方个原子,这小于前面所说的范围)这个函数的值可以看成是不大于4的,所以并查集的操作可以看作是线性的。它支持以下三中种操作:

  -Union (Root1, Root2) //并操作;把子集合Root2并入集合Root1中.要求:Root1和 Root2互不相交,否则不执行操作.

  -Find (x) //搜索操作;搜索单元素x所在的集合,并返回该集合的名字.

  -UFSets (s) //构造函数。将并查集中s个元素初始化为s个只有一个单元素的子集合.

  -对于并查集来说,每个集合用一棵树表示。

  -集合中每个元素的元素名分别存放在树的结点中,此外,树的每一个结点还有一个指向其双亲结点的指针。

  -设 S1= {0, 6, 7, 8 },S2= { 1, 4, 9 },S3= { 2, 3, 5 }

-为简化讨论,忽略实际的集合名,仅用表示集合的树的根来标识集合。

  -为此,采用树的双亲表示作为集合存储表示。集合元素的编号从0到 n-1。其中 n 是最大元素个数。在双亲表示中,第 i 个数组元素代表包含集合元素 i 的树结点。根结点的双亲为-1,表示集合中的元素个数。为了区别双亲指针信息( ≥ 0 ),集合元素个数信息用负数表示。   

下标

parent

集合S1, S2和S3的双亲表示:

            S1 ∪ S2的可能的表示方法

const int DefaultSize = 10;

  class UFSets { //并查集的类定义

  private:

   int *parent;

   int size;

  public:

   UFSets ( int s = DefaultSize );

   ~UFSets ( ) { delete [ ] parent; }

   UFSets & operator = ( UFSets const & Value );//集合赋值

   void Union ( int Root1, int Root2 );

   int Find ( int x );

   void UnionByHeight ( int Root1, int Root2 ); };

   UFSets::UFSets ( int s ) { //构造函数

   size = s;

   parent = new int [size+1];

   for ( int i = 0; i <= size; i++ ) parent[i] = -1;

  }

  unsigned int UFSets::Find ( int x ) { //搜索操作

   if ( parent[x] <= 0 ) return x;

   else return Find ( parent[x] );

  }

  void UFSets::Union ( int Root1, int Root2 ) { //并

   parent[Root2] = Root1; //Root2指向Root1

  }

Find和Union操作性能不好。假设最初 n 个元素构成 n 棵树组成的森林,parent[i] = -1。做处理Union(0, 1), Union(1, 2), …, Union(n-2, n-1)后,将产生如图所示的退化的树。

执行一次Union操作所需时间是O(1),n-1次Union操作所需时间是O(n)。若再执行Find(0), Find(1), …, Find(n-1), 若被

搜索的元素为i,完成Find(i)操作需要时间为O(i),完成 n 次搜索需要的总时间将达到

              

Union操作的加权规则

  为避免产生退化的树,改进方法是先判断两集合中元素的个数,如果以 i 为根的树中的结点个数少于以 j 为根的树中的结点个数,即parent[i] > parent[j],则让 j 成为 i 的双亲,否则,让i成为j的双亲。此即Union的加权规则。

parent[0](== -4) < parent[4] (== -3)

  void UFSets::WeightedUnion(int Root1, int Root2) {

   //按Union的加权规则改进的算法

   int temp = parent[Root1] + parent[Root2];

   if ( parent[Root2] < parent[Root1] ) {

    parent[Root1] = Root2; //Root2中结点数多

    parent[Root2] = temp;  //Root1指向Root2

   }

   else {

    parent[Root2] = Root1; //Root1中结点数多

    parent[Root1] = temp;  //Root2指向Root1

   }

  }

 使用加权规则得到的树

引题——亲戚(relation)

【问题描述】若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。

规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。(人数≤5000,亲戚关系≤5000,询问亲戚关系次数≤5000)。

【算法分析】

1. 算法1,构造图论模型。

用一个n*n的二维数组描述上面的图形,记忆各个点之间的关系。然后,只要判断给定的两个点是否连通则可知两个元素是否有“亲戚”关系。

但要实现上述算法,我们遇到两个困难:

(1)空间问题:需要n2的空间,而n高达5000!

(2)时间问题:每次判断连通性需要O(n)的处理。

该算法显然不理想。

并查集多用于图论问题的处理优化,我们看看并查集在这里的表现如何。

2. 算法2,并查集的简单处理。

我们把一个连通块看作一个集合,问题就转化为判断两个元素是否属于同一个集合。

假设一开始每个元素各自属于自己的一个集合,每次往图中加一条边a-b,就相当于合并了两个元素所在集合A和B,因为集合A中的元素用过边a-b可以到达集合B中的任意元素,反之亦然。

当然如果a和b本来就已经属于同一个集合了,那么a-b这条边就可以不用加了。

(1)具体操作:

① 由此用某个元素所在树的根结点表示该元素所在的集合;

② 判断两个元素时候属于同一个集合的时候,只需要判断他们所在树的根结点是否一样即可;

③ 也就是说,当我们合并两个集合的时候,只需要在两个根结点之间连边即可。

(2)元素的合并图示:

(3)判断元素是否属于同一集合:

用father[i]表示元素i的父亲结点,如刚才那个图所示:

faher[1]:=1;faher[2]:=1;faher[3]:=1;faher[4]:=5;faher[5]:=3

至此,我们用上述的算法已经解决了空间的问题,我们不再需要一个n2的空间来记录整张图的构造,只需要用一个记录数组记录每个结点属于的集合就可以了。

但是仔细思考不难发现,每次询问两个元素是否属于同一个集合我们最多还是需要O(n)的判断!

3. 算法3,并查集的路径压缩。

算法2的做法是指就是将元素的父亲结点指来指去的在指,当这课树是链的时候,可见判断两个元素是否属于同一集合需要O(n)的时间,于是路径压缩产生了作用。

路径压缩实际上是在找完根结点之后,在递归回来的时候顺便把路径上元素的父亲指针都指向根结点。

这就是说,我们在“合并5和3”的时候,不是简单地将5的父亲指向3,而是直接指向根节点1,由此我们得到了一个复杂度只是O(1)的算法。

〖程序清单〗

(1)初始化:

for i:=1 to n do father[i]:=i;

因为每个元素属于单独的一个集合,所以每个元素以自己作为根结点。

(2)寻找根结点编号并压缩路径:

function getfather(v : integer) : integer;

begin

if father[v]=v then exit(v);

father[v]:=getfather(father[v]);

getfather:=father[v];

end;

(3)合并两个集合:

proceudre merge(x, y : integer);

begin

x:=getfather(x);

y:=getfather(y);

father[x]:=y;

end;

(4)判断元素是否属于同一结合:

function judge(x, y : integer) : boolean;

begin

x:=getfaher(x);

y:=gefather(y);

if x=y then exit(true)

else exit(false);

end;

这个的引题已经完全阐述了并查集的基本操作和作用。

三、并查算法

通过对上面引题的分析,我们已经十分清楚——所谓并查集算法就是对不相交集合(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、make_set(x) 把每一个元素初始化为一个集合
建立一个新的集合,其中集合只有唯一的一个元素x2、union_set(x, y) 按秩合并x,y所在的集合3、find_set(x)返回x所在的集合的代表 在执行查找操作时,要沿着父节点指针一直找下去,直到找到树根为止。大家要注意途中的箭头。
4、实现并查集的标准代码: 1 #include <stdio.h>2 3 const int MAXN = 100; /*结点数目上线*/4 int pa[MAXN];    /*p[x]表示x的父节点*/5 int rank[MAXN];    /*rank[x]是x的高度的一个上界*/6 7 void make_set(int x)8 {/*创建一个单元集*/9     pa[x] = x;
10     rank[x] = 0;
11 }
12
13 int find_set(int x)
14 {/*带路径压缩的查找*/
15     if(x != pa[x])
16         pa[x] = find_set(pa[x]);
17     return pa[x];
18 }
19
20 /*按秩合并x,y所在的集合*/
21 void union_set(int x, int y)
22 {
23     x = find_set(x);
24     y = find_set(y);
25     if(rank[x] > rank[y])/*让rank比较高的作为父结点*/
26         pa[y] = x;
27     else
28     {
29         pa[x] = y;
30         if(rank[x] == rank[y])
31             rank[y]++;
32     }
33 }

并查集详细讲解(转载) 模板相关推荐

  1. 看了必懂的并查集原理(转载)

    看了必懂的并查集原理(转载) 并查集是我暑假从高手那里学到的一招,觉得真是太精妙的设计了.以前我无法解决的一类问题竟然可以用如此简单高效的方法搞定.不分享出来真是对不起party了.(party:我靠 ...

  2. 并查集及路径压缩模板

    并查集那最简单的应用就是合并两个元素和查询两个元素是否属于同一集体,它的工作过程如下:首先将每个元素放在单独的一个集合里,集合的名字就是这个元素的编号,对于查询操作就找两个元素的所在集合编号,集合编号 ...

  3. python阿凡提与国王下棋_实例:阿基米德与国王下棋的故事_【一图胜千言】以图讲解Python序列之语言基础与提升大合集详细讲解(含500条笔记)_Python视频-51CTO学院...

    课程亮点: 一图胜千言,让文科生都能看得懂的python教程!!!另外加详细的笔记作为辅助工具,500多条笔记帮助学员学习Python500多个知识点 课程内容: 1. Python3语言总体介绍以及 ...

  4. (并查集模板)AcWing 模板+例题240.食物链

    并查集最简单的模板 #include <bits/stdc++.h>using namespace std; int p[100010]; int n,m; int Find(int x) ...

  5. 每日一题——洛谷 P1551 亲戚 (并查集)

    大家好,我是爬行系,今天打卡并查集相关例题 文章目录 并查集 1.概念 2.模板 例题 1.题目描述 2.AC代码 更多练习题 总结 并查集 1.概念 并查集的思想是用一个数组表示了整片森林(pare ...

  6. 超有爱的并查集 ------ 转

    发现一个篇特别好特别好的并查集基础讲解 原文链接:飘过的小牛-超有爱的并查集 例子就是杭电上的畅通工程: http://acm.hdu.edu.cn/showproblem.php?pid=1232 ...

  7. 无向图的连通分支数(并查集)

    目录 这是牛客网上题号为 KY268 的一道题 题目描述:该题的目的是要你统计图的连通分支数. 输入描述:每个输入文件包含若干行,每行两个整数 i, j,表示节点 i 和 j 之间存在一条边. 输出描 ...

  8. [bzoj4998][LCT][并查集]星球联盟

    Description 在遥远的S星系中一共有N个星球,编号为1-N.其中的一些星球决定组成联盟,以方便相互间的交流.但是,组成 联盟的首要条件就是交通条件.初始时,在这N个星球间有M条太空隧道.每条 ...

  9. 「图论」第1章 并查集课堂过关

    文章目录 A. [例题1][模板]并查集 题目 代码 B. [例题2]程序自动分析 题目 代码 C. [例题3]银河英雄传说 题目 题目背景 题目描述 输入格式 输出格式 输入输出样例 说明/提示 思 ...

  10. c++自带的可持久化平衡树?rope大法好!(超详细解答 + 5道例题讲解,可直接替代可持久化的线段树、并查集、平衡树!)

    整理的算法模板合集: ACM模板 目录 c++自带的可持久化平衡树?rope大法好! 1. 声明 2. 支持操作 char类型的rope int类型的rope 3. 具体的细节 4. "可持 ...

最新文章

  1. Discuz NT 架构剖析之Config机制
  2. 计算机基础及wps office应用_全国2019年4月自考00018《计算机应用基础》试题
  3. [Grid Layout] Specify a grid gutter size with grid-gap
  4. 搞不懂SDN和SD-WAN?那是因为你没看这个小故事—Vecloud微云
  5. OpenCV直方图均衡化
  6. mysql 分段解析_MYSQL分段统计
  7. redis安装配置参考
  8. mysql设置主键可视化_mysql怎么设置主键自
  9. Fast-RTPS初体验
  10. html文件文本预处理,HTML文件文本信息预处理技术.pdf
  11. IE8自动提交form的问题
  12. kaggle 电商数据分析
  13. Byethost美国免费空间免费撸
  14. 办公未来已来,金山WPS如何从“追随者”到“领跑者”
  15. 携程实习生春招面经-后台开发
  16. 1024人工智能和大数据应用高峰论坛
  17. AMD提出的补丁使退出延迟降低21%左右
  18. 安装Mariadb columnStore(10.3版本)
  19. 【华为OD机试真题 JAVA】找城市
  20. 韩国电信为印尼带来5G技术

热门文章

  1. ArcGIS JS 学习笔记2 实现仿百度的拖拽画圆
  2. pandas把'm8[ns]'类型转换为int类型进行运算
  3. 二叉树转为单链表——Flatten Binary Tree to Linked List
  4. CSS3 transform 属性
  5. 10_android打包的过程
  6. 【Java Saves!】Session 2:我的意图
  7. 企业信息化之“请纵向排队”
  8. 如何把jpg转换成word文档
  9. 帝国cms后台不停的登录成功
  10. SQLServer To MySQL 解决方案