收藏 分享(赏)

并查集及例题题解.doc

上传人:11xg27ws 文档编号:9509484 上传时间:2019-08-11 格式:DOC 页数:9 大小:59.50KB
下载 相关 举报
并查集及例题题解.doc_第1页
第1页 / 共9页
并查集及例题题解.doc_第2页
第2页 / 共9页
并查集及例题题解.doc_第3页
第3页 / 共9页
并查集及例题题解.doc_第4页
第4页 / 共9页
并查集及例题题解.doc_第5页
第5页 / 共9页
点击查看更多>>
资源描述

1、树结构的并查集 采用树结构支持并查集的计算能够满足我们的要求。并查集与一般的树结构不同,每个顶点纪录的不是它的子结点,而是将它的父结点记录下来。下面是树结构的并查集的两种运算方式 直接在树中查询 边查询边“路径压缩” 对应与前面的链式存储结构,树状结构的优势非常明显:编程复杂度低;时间效率高。 直接在树中查询 集合的合并算法很简单,只要将两棵树的根结点相连即可,这步操作只要 O(1)时间复杂度。算法的时间效率取决于集合查找的快慢。而集合的查找效率与树的深度呈线性关系。因此直接查询所需要的时间复杂度平均为 O(logN) 。但在最坏情况下,树退化成为一条链,使得每一次查询的算法复杂度为 O(N

2、) 。 边查询边“路径压缩 其实,我们还能将集合查找的算法复杂度进一步降低:采用“路径压缩”算法。它的想法很简单:在集合的查找过程中顺便将树的深度降低。采用路径压缩后,每一次查询所用的时间复杂度为增长极为缓慢的 ackerman 函数的反函数(x) 。对于可以想象到的n,(n)都是在 5 之内的。并查集:(union-find sets)是一种简单的用途广泛的集合. 并查集是若干个不相交集合,能够实现较快的合并和判断元素所在集合的操作,应用很多。一般采取树形结构来存储并查集,并利用一个 rank 数组来存储集合的深度下界,在查找操作时进行路径压缩使后续的查找操作加速。这样优化实现的并查集,空间

3、复杂度为 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 所在的集合,并返回该集

4、合的名字 .UFSets (s) /构造函数。将并查集中 s 个元素初始化为 s 个只有一个单元素的子集合.对于并查集来说,每个集合用一棵树表示。集合中每个元素的元素名分别存放在树的结点中,此外,树的每一个结点还有一个指向其双亲结点的指针。设 S1= 0, 6, 7, 8 ,S2= 1, 4, 9 ,S3= 2, 3, 5 为简化讨论,忽略实际的集合名,仅用表示集合的树的根来标识集合。为此,采用树的双亲表示作为集合存储表示。集合元素的编号从 0 到 n-1。其中 n 是最大元素个数。在双亲表示中,第 i 个数组元素代表包含集合元素 i 的树结点。根结点的双亲为-1,表示集合中的元素个数。为了区

5、别双亲指针信息( 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 /集合赋值void Union ( int Root1, int Root2 );int Find ( int x );void UnionByHeight

6、( int Root1, int Root2 ); ;UFSets:UFSets ( int s ) /构造函数size = s;parent = new int size+1;for ( int i = 0; i parentj,则让 j 成为 i 的双亲,否则,让 i 成为 j 的双亲。此即 Union 的加权规则。parent0(= -4) parent4 (= -3)void UFSets:WeightedUnion(int Root1, int Root2) /按 Union 的加权规则改进的算法int temp = parentRoot1 + parentRoot2;if ( pa

7、rentRoot2 parentRoot1 ) parentRoot1 = Root2; /Root2 中结点数多parentRoot2 = temp; /Root1 指向 Root2else parentRoot2 = Root1; /Root1 中结点数多parentRoot1 = temp; /Root2 指向 Root1使用加权规则得到的树引题亲戚(relation)【问题描述】若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。规定:x 和 y 是亲戚,y 和 z 是亲戚,那么 x 和 z 也是亲戚。如果 x,y

8、是亲戚,那么 x 的亲戚都是 y 的亲戚,y 的亲戚也都是 x 的亲戚。 (人数5000,亲戚关系5000,询问亲戚关系次数5000) 。【算法分析】1. 算法 1,构造图论模型。用一个 n*n 的二维数组描述上面的图形,记忆各个点之间的关系。然后,只要判断给定的两个点是否连通则可知两个元素是否有“亲戚”关系。 但要实现上述算法,我们遇到两个困难: (1)空间问题:需要 n2 的空间,而 n 高达 5000!(2)时间问题:每次判断连通性需要 O(n)的处理。该算法显然不理想。并查集多用于图论问题的处理优化,我们看看并查集在这里的表现如何。2. 算法 2,并查集的简单处理。我们把一个连通块看作

9、一个集合,问题就转化为判断两个元素是否属于同一个集合。假设一开始每个元素各自属于自己的一个集合,每次往图中加一条边 ab,就相当于合并了两个元素所在集合 A 和 B,因为集合 A 中的元素用过边 ab 可以到达集合 B 中的任意元素,反之亦然。当然如果 a 和 b 本来就已经属于同一个集合了,那么 a-b 这条边就可以不用加了。(1)具体操作: 由此用某个元素所在树的根结点表示该元素所在的集合; 判断两个元素时候属于同一个集合的时候,只需要判断他们所在树的根结点是否一样即可; 也就是说,当我们合并两个集合的时候,只需要在两个根结点之间连边即可。(2)元素的合并图示:(3)判断元素是否属于同一集

10、合:用 fatheri表示元素 i 的父亲结点,如刚才那个图所示:faher1:=1;faher2:=1;faher3:=1;faher4:=5;faher5:=3至此,我们用上述的算法已经解决了空间的问题,我们不再需要一个 n2 的空间来记录整张图的构造,只需要用一个记录数组记录每个结点属于的集合就可以了。但是仔细思考不难发现,每次询问两个元素是否属于同一个集合我们最多还是需要 O(n)的判断!3. 算法 3,并查集的路径压缩。算法 2 的做法是指就是将元素的父亲结点指来指去的在指,当这课树是链的时候,可见判断两个元素是否属于同一集合需要 O(n)的时间,于是路径压缩产生了作用。路径压缩实际

11、上是在找完根结点之后,在递归回来的时候顺便把路径上元素的父亲指针都指向根结点。这就是说,我们在“合并 5 和 3”的时候,不是简单地将 5 的父亲指向 3,而是直接指向根节点 1,由此我们得到了一个复杂度只是 O(1)的算法。程序清单(1)初始化:for i:=1 to n do fatheri:=i;因为每个元素属于单独的一个集合,所以每个元素以自己作为根结点。(2)寻找根结点编号并压缩路径:function getfather(v : integer) : integer;beginif fatherv=v then exit(v);fatherv:=getfather(fatherv);

12、getfather:=fatherv;end;(3)合并两个集合:proceudre merge(x, y : integer);beginx:=getfather(x);y:=getfather(y);fatherx:=y;end;(4)判断元素是否属于同一结合:function judge(x, y : integer) : boolean;beginx:=getfaher(x);y:=gefather(y);if x=y then exit(true)else exit(false);end;这个的引题已经完全阐述了并查集的基本操作和作用。三、并查算法通过对上面引题的分析,我们已经十分清

13、楚所谓并查集算法就是对不相交集合(disjoint set)进行如下两种操作:(1)检索某元素属于哪个集合;(2)合并两个集合。我们最常用的数据结构是并查集的森林实现。也就是说,在森林中,每棵树代表一个集合,用树根来标识一个集合。有关树的形态在并查集中并不重要,重要的是每棵树里有那些元素。1. 合并操作为了把两个集合 S1 和 S2 并起来,只需要把 S1 的根的父亲设置为 S2 的根(或把 S2 的根的父亲设置为 S1 的根)就可以了。这里有一个优化:让深度较小的树成为深度较大的树的子树,这样查找的次数就会少些。这个优化称为启发式合并。可以证明:这样做以后树的深度为 O(logn)。即:在一

14、个有 n 个元素的集合,我们将保证移动不超过 logn 次就可以找到目标。【证明】我们合并一个有 i 个结点的集合和一个有 j 个结点的集合,我们设 ij,我们在一个小的集合中增加一个被跟随的指针,但是他们现在在一个数量为 ij 的集合中。由于:1+log i=log(i+i)=log(i+j);所以我们可以保证性质。由于使用启发式合并算法以后树的深度为 O(logn),因此我们可以得出如下性质:启发式合并最多移动 2logn 次指针就可以决定两个事物是否想联系。同时我们还可以得出另一个性质:启发式快速合并所得到的集合树,其深度不超过 ,其中n 是集合 S 中的所有子集所含的成员数的总和。【证

15、明】我们可以用归纳法证明:当 i=1 时,树中只有一个根节点,即深度为 1又|log2 1|+1=1 所以正确。假设 in1 时成立,尝试证明 in 时成立。不失一般性,可以假设此树是由含有 m(1m n/2)个元素,根为 j 的树 Sj,和含有nm 个元素、根为 k 的树 Sk 合并而得到,并且,树 j 合并到树 k,根是 k。(1)若合并前:子树 Sj 的深度子树 Sk 的深度则合并后的树深度和 Sk 相同,深度不超过:|log2(n-m)|+1显然不超过|log2 n|+1;(2)若合并前:子树 Sj 的深度子树 Sk 的深度则合并后的树的深度为 Sj 的深度+1,即:(|log2m|+

16、1)+1=|log2(2m)|+1=|log2n|+1 小结:实践告诉我们,上面所陈述的性质对于一个 m 条边 n 个事物的联系问题,最多执行mlogn 次指令。我们只是增加了一点点额外的代码,我们就把程序的效率很大地提升了。大量的实验可以告诉我们,启发式合并可以在线形时间内解答问题。更确切地说,这个算法运行时间的花费,很难再有更加明显的优秀、高效的算法了。2. 查找操作查找一个元素 u 也很简单,只需要顺着叶子到根结点的路径找到 u 所在的根结点,也就是确定了 u 所在的集合。这里又有一个优化:找到 u 所在树的根 v 以后,把从 u 到 v 的路径上所有点的父亲都设置为 v,这样也会减少查

17、找次数。这个优化称作路径压缩(compresses paths) 。压缩路径可以有很多种方法,这里介绍两种最常用的方法:(1)满路径压缩(full compresses paths):这是一种极其简单但又很常用的方法。就是在添加另一个集合的时候,把所有遇到的结点都指向根节点。(2)二分压缩路径(compresses paths by halving):具体思想就是把当前的结点,跳过一个指向父亲的父亲,从 6 而使整个路径减半深度减半。这种办法比满路径压缩要快那么一点点。数据越大,当然区别就会越明显。压缩路径的本质使路径深度更加地减小,从而使访问的时候速度增快,是一种很不错的优化。在使用路径压缩

18、以后,由于深度经常性发生变化,因此我们不再使用深度作为合并操作的启发式函数值,而是使用一个新的 rank 数。刚建立的新集合的 rank 为 0,以后当两个rank 相同的树合并时,随便选一棵树作为新根,并把它的 rank 加 1;否则 rank 大的树作为新根,两棵树的 rank 均不变。3. 时间复杂度并查集进行 n 次查找的时间复杂度是 O(n )(执行 n-1 次合并和 mn 次查找) 。其中 是一个增长极其缓慢的函数,它是阿克曼函数(Ackermann Function)的某个反函数。它可以看作是小于 5 的。所以可以认为并查集的时间复杂度几乎是线性的。通过上面的分析,我们可以得出:并查集适用于所有集合的合并与查找的操作,进一步还可以延伸到一些图论中判断两个元素是否属于同一个连通块时的操作。由于使用启发式合并和路径压缩技术,可以讲并查集的时间复杂度近似的看作 O(1),空间复杂度是 O(N),这样就将一个大规模的问题转变成空间极小、速度极快的简单操作。本文来自 CSDN 博客,转载请标明出处:http:/

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 企业管理 > 管理学资料

本站链接:文库   一言   我酷   合作


客服QQ:2549714901微博号:道客多多官方知乎号:道客多多

经营许可证编号: 粤ICP备2021046453号世界地图

道客多多©版权所有2020-2025营业执照举报