并查集(吴)

相关主题
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

三、并查集支持的操作 并查集的数据结构记录了一组分离的动态集合S, S={S1,S2,…,Sk},每个集合通过一个“代表”加 以识别,代表即该集合中的某个元素(成员),哪
一个成员被选做代表是无所谓的,重要的是:如果
求某一动态集合的代表两次,且在两次请求间不修
改此集合,则两次得到的答案应该是相同的。
请写一个程序,对于我们的关于亲戚关系的提 问,以最快的速度给出答案。
输入: 由两部分组成: 第一部分以N,M开始。N为问题涉及的人的个数(1 N 20000)。这些人的编号为1,2,3,…, N。下面有M行 (1 M 1 000 000),每行有两个数ai, bi,表示已知 ai和bi是亲戚。 第二部分以Q开始。以下Q行有Q个询问(1 Q 1 000 000),每行为ci, di,表示询问ci和di是否为亲戚。 输出:对于每个询问ci, di,输出一行:若ci和di为亲戚, 则输出“Yes”,否则输出“No”。
UNION(x,y): 假设UNION(x,y)的参数是有序的,即把y属于的 集合合并到x的集合 有两种实现方法:
(1)简单实现
不考虑任何因素,出现FIND-SET(x)≠ FIND-SET(y)时,
直接将y的表头接到x的表尾,同时将y中所在集合元素的head值设 为FIND-SET(x)。同时x的表尾也应该设为原y表的表尾。
being c[i]:=j ; { 保存生成树第i条边 , 因为j 是GE数组的下 标} i:=i+1 ; s[m1]:=s[m1]+s[m2] ; s[m2]:=[ ] ; end; (3) j:=j+1 ; end ; 运行该程序后,C数组的值为:
1
1
2
2
3
3
5
4
7
5
问题2、 亲戚(relation) 问题描述:或许你并不知道,你的某个朋友是你的亲 戚。他可能是你的曾祖父的外公的女婿的外甥女的表 姐的孙子! 如果能得到完整的家谱,判断两个人是否亲戚应该 是可行的,但如果两个人的最近公共祖先与他们相隔 好几代,使得家谱十分庞大,那么检验亲戚关系实非 人力所能及。在这种情况下,最好的帮手就是计算机。 为了将问题简化,你将得到一些亲戚关系的信息,如 Marry和Tom是亲戚,Tom和Ben是亲戚,等等。从这些 信息中,你可以推出Marry和Ben是亲戚。
并查集
一、什么叫并查集 并查集是一种用于分离集合操作的抽象数据类型。 它所处理的是“集合”之间的关系,即动态地维护和 处理集合元素之间复杂的关系,当给出两个元素的一 个无序对(a,b)时,需要快速“合并”a和b分别所在 的集合,这其间需要反复“查找”某元素所在的集合。
“并”、“查”和“集”三字由此而来。在这种数据
类型中,n个不同的元素被分为若干组。每组是一个 集合,这种集合叫做分离集合。并查集支持查找一个
元素所属的集合以及两个元素各自所属的集合的合并。
二、并查集的基本思想
初始时n个元素分属不同的n个集合,通过不断的 给出元素间的联系,要求实时的统计元素间的关系 (直接或间接的联系),可用并查集加以判断。 (1)元素间是否有联系:判断两个元素是否属于同 一个集合;(查) (2)建立元素间的联系:只需合并两个元素各自所 属的集合。(并) 并查集本身不具有结构,必须借助一定的数据结 构以得到支持和实现。一般用的比较多的是数组、链 表和树来实现。
2009年夏令营 高级数据结构讲座之二
并查集及其应用
2009年 高级数据结构
2009年5月30日
1、研究Kruskal最小生成树算法
假设G=(V,E)是一个具有n个顶点的连通图, T=(U,TE)是G的最小生成树,u的初始值等于v,
即包含有G中的全部顶点,TE的初值为空,此算法
的基本思想是:将图G中的边按权值从小到大的顺序 依次选取数,若选取的边使生成树T不形成回路,则 把它并入TE中,保留作为T的一条边,若选取的边 使生成树T形成回路,则将其舍去,如此进行下去,
算法的实现:
(1)用边集数组存放图的结构:GE,记录数组
(2)实现边的排序:按边的权值进行
(3)用集合数组S存放连通的顶点集合
(4)用C数组存放生成的第i条边属于GE边集数组的第X 条边下标序号。
算法: Procedure kruskal ( GE,C ) ; begin for i:=1 to n do s[i]:= [i ] ; { 初始化顶点集合 } i:=1 ; j:=1 { i 表示边数,j 表示数组GE的下标 } while i<= n-1 do [ (1) for k:=1 to n do begin if GE[J].fr in s[k] then m1:=k ; if GE[J].ed in s[k] then m2:=k ; end ; { 记录第j条边的两个端点的集合序号} (2) if m1<> m2 then { 生成树的一条边 }
Hale Waihona Puke Baidu
MAKE-SET(x):S[x].head = x;S[x].next = 0;
FIND-SET(x):return S[x].head 两个过程的时间复杂度都为O(1)。 采用链表时,当有两个元素(x,y),若FIND-SET(x) ≠ FIND-SET(y),则两者对应不同的集合,需要将两 个链表合并,做法是将一个表的表头直接接到另一个表 的表尾,这一步操作看似很简单,但势必造成修改后需 要把接上去的那个表的所有head值修改,这需要线性的 赋值操作,其复杂度与选择接在尾部的链表长度成正比。
直到TE中包含有n-1条边为止,此时的T即为最小
生成树。
此经典算法的思想是将树上的边按照
权排序,然后从小到大分析每一条边,如
果选到一条边e=(v1,v2),且v1和v2不
在一个连通块中,就将e作为最小生成树
的一条边,否则忽略e。 Kruskal 算法
的关键在于如何判断是否构成回路。
判断最小生成树在构成过程中,是否构成回路的方法: 方法:将各个顶点划分所属集合的方法解决。每个集合的 顶点表示一个无回路的连通分量。 (1) 初始化:{V1}, {V2}, {V3}, {V4}, {V5}, {V6} (2)当从边集数组中按次序选择一条边时,若它的两个 端点分别属于两个不同的集合,则表明该边连接了两个不 同的连通分量,无回路,该边作为树的一条边,将端点所 在的两个集合合并为一个集合,此时它们成为一个连通分 量。 (3)若选择的边的两个端点在一个集合中,则放弃该边, 否则造成回路。 {V1,V5}, {V2,V3,V4}, {V6} {V1,V5}, {V2,V3,V4,V6} {V1,V5,V2,V3,V4,V6}
输入关系 初始状态 (2,4) (5,7)
分离集合 {1}{2}{3}{4}{5}{6}{7}{8}{9}{10} {1}{2,4}{3}{5}{6}{7}{8}{9}{10} {1}{2,4}{3}{5,7}{6}{8}{9}{10}
(1,3)
(8,9) (1,2) (5,6) (2,3)
{1,3}{2,4}{5,7}{6}{8}{9}{10}
得到了一个N个顶点M条边的图论模型,注意到传递关 系,在此无向图中的一个连通块中的任意点之间都是 亲戚。对于最后的Q个提问,即判断所提问的两个顶点 是否在同一个连通块中。
对于输入的N个点M条边,建立一个无向图找出所有 的连通块(遍历算法),然后根据提问逐个进行判断。 但是本题的数据范围很大,1 N 20000 , 1 M 1 000 000,不管是从空间还是时间上看,都很难 接受。 考虑能否用集合的思路来解决该问题。 对于每个人建立一个集合,开始的时候集合元素是 这个人本身,表示开始时不知道任何人是他的亲戚。以 后每次给出一个亲戚关系时,就将两个集合合并。这样 实时地得到了在当前状态下的集合关系。如果有提问, 即在当前得到的结果中看两元素是否属于同一集合。 对于样例数据的解释如下图:
实现简单,实际使用较多。但是合并的代价太大,在最
坏情况下,所有集合合并成一个集合的总代价达到O(n2)。
并查集的数组实现
UNION(x,y)过程演示:
输入样例: 10 7 2 4 5 7 1 3 8 9 1 2 5 6 2 3 3 3 4 7 10 8 9
S
1
1
2
2
3
3
4
4
5
5
6
6
7
7
8
8
9 10
输入样例(relation.in): 10 7 输 出 样 2 4 (relation.out): Yes 5 7 No 1 3 Yes 8 9 1 2 5 6 2 3 3 3 4 7 10 8 9

问题分析: 将每个人抽象成为一个点,数据给出M个边的关系
,两个人是亲戚的时候两点间有一条边。很自然的就
注意:last指针其实只要在表头结点中记录即可,因为每一
次查到FIND-SET(x)都可以得到表头元素。而链表中其他元素重 新记录last是无意义的。 我们总是把y接到x里去,那么如果y所在的集合非常大,每次 赋值的代价就会非常高,考虑输入数据的特殊性,比如出现输入 为: (2,1),(3,1),(4,1),(5,1),(6,1) , … ,(n,1) 最坏情况下时间复杂度为O(n^2)。
四、并查集的实现及优化 1、 并查集的数组实现 用数组s[i]记录元素i所属集合的编号。 MAKE-SET(x):初始化只要s[i]:=i
FIND-SET(x):查找元素所属的集合时,只需读出
s[i],时间复杂度为O(1)。 UNION(x,y):合并两元素各自所属的集合时,需要 将数组中属于其中一个集合的元素所对应的数组元素值全 部改为另一个集合的编号值,时间复杂度为O(n)。
动态集合中的每一元素是由一个对象来表示 的,设x表示一个对象,并查集的实现需要支 持如下操作:
① MAKE-SET(x)——建立分离集合 建立一个新的集合,其仅有的成员(同时就是代 表)是x。由于各集合是分离的,要求x没有在其它集 合中出现过。 ② UNION(x,y)——集合的并集操作 将包含x和y的动态集合(例如Sx和Sy)合并为一 个新的集合,假定在此操作前这两个集合是分离的。 结果的集合代表是Sx∪Sy的某个成员。一般来说,在 不同的实现中通常都以Sx或者Sy的代表作为新集合的 代表。此后,由新的集合S代替了原来的Sx和Sy。 ③ FIND-SET(x)——集合元素的查找 返回一个包含x的集合的代表。
{1,3}{2,4}{5,7}{6}{8,9}{10} {1,2,3,4}{5,7}{6}{8,9}{10} {1,2,3,4}{5,6,7}{8,9}{10} {1,2,3,4}{5,6,7}{8,9}{10}
并查集及其应用
由上图可以看出,操作是在集合的基础上 进行的,操作过程中我们没有保存任何一条 边,而且每一步得到的划分方式是动态的。 这就是所要介绍的数据结构知识 :
可以证明这种方法的效率。当有n个元素时,在 UNION上的花费(即重新赋值的次数)的上界是 O(nlog2n)。考虑一个固定的对象x,当x的代表指针 (head)被更新时,x必是属于一个较小的集合,因 此,x的代表指针第一次更新后,结果集合必然至少 有2个元素,类似的,下一次更新后,x所在的集合至 少有4个元素。继续下去,可以发现,x的代表指针最 多被更新log2n次,因为当x所在集合元素已经等于n以 后,不可能再发生UNION操作。所以,总共有n个元素 时,操作的总次数不超过nlog2n次。这就保证了整个 算法的复杂度是理想的。
(2)快速实现 上述简单实现非常不理想,针对y可能比较大的这个 问题,可以很快产生一个聪明的想法:不妨比较x和y所 在集合的大小,从而作出选择,把较短的链表接在较长 的尾部,这样效果是一样的,但耗费肯定不比原来差。 这就是快速实现的思路。可以在node里多设一个域 number,用来记录此条链表中成员的个数。显然number 记录在表头元素中即可,将两表合并的时候,只要将表 的number相加,因此维护起来是非常方便的。 这种快速实现的方法可以称为加权启发式合并,这 里的权就是指所记录的number。
9 10
S
1
1
1
2
1
3
1
4
5
5
5
6
5
7
8
8
8 10
9 10
2、 并查集的链表实现
每个集合对应一个链表,它有一个表头,每一个元 素有一个指针指向表头,表明了它所属的类,另有一个 指针指向它的下一个元素,同时为了方便实现,再设一 个指针last表示链表中的最后一个元素(表尾)。 当然,由于处理的对象一般都是连续整数,所以,可 以选择静态数组(通过下标)来模拟实现,如: type node = record head,next,last:integer; end; var S : array[1..maxn] of node;
相关文档
最新文档