您好,欢迎访问三七文档
并查集及其应用一、引例例一、亲戚(relation)问题描述:或许你并不知道,你的某个朋友是你的亲戚。他可能是你的曾祖父的外公的女婿的外甥女的表姐的孙子!!!如果能得到完整的家谱,判断两个人是否亲戚应该是可行的,但如果两个人的最近公共祖先与他们相隔好几代,使得家谱十分庞大,那么检验亲戚关系实非人力所能及。在这种情况下,最好的帮手就是计算机。为了将问题简化,你将得到一些亲戚关系的信息,如Marry和Tom是亲戚,Tom和Ben是亲戚,等等。从这些信息中,你可以推出Marry和Ben是亲戚。请写一个程序,对于我们的关于亲戚关系的提问,以最快的速度给出答案。并查集及其应用输入由两部分组成:第一部分以N,M开始。N为问题涉及的人的个数(1N20000)。这些人的编号为1,2,3,…,N。下面有M行(1M1000000),每行有两个数ai,bi,表示已知ai和bi是亲戚。第二部分以Q开始。以下Q行有Q个询问(1Q1000000),每行为ci,di,表示询问ci和di是否为亲戚。输出:对于每个询问ci,di,输出一行:若ci和di为亲戚,则输出“Yes”,否则输出“No”。并查集及其应用输入样例(relation.in):1072457138912562333471089输出样例(relation.out):YesNoYes并查集及其应用问题分析:将每个人抽象成为一个点,数据给出M个边的关系,两个人是亲戚的时候两点间有一条边。很自然的就得到了一个N个顶点M条边的图论模型,注意到传递关系,在图中一个连通块中的任意点之间都是亲戚。对于最后的Q个提问,即判断所提问的两个顶点是否在同一个连通块中。用传统的思路,马上可以反应过来,对于输入的N个点M条边,找出连通块,然后进行判断。但这种实现思路首先必须保存M条边,然后再进行普通的遍历算法,不管是从空间还是时间上看,效率都不高。再进一步考虑,如果把题目的要求改一改,对于边和提问相间输入,即把题目改成:并查集及其应用第一行是N,M。N为问题涉及的人的个数(1N20000)。这些人的编号为1,2,3,…,N。下面有M行(1M2000000),每行有三个数ki,ai,bi,ai和bi表示两个元素,ki为0或1,ki为1时表示这是一条边的信息,即a表示i和bi是亲戚关系;ki为0时表示这是一个提问,要你根据此行以前所得到的信息,判断ai和bi是否是亲戚,对于每条提问回答Yes或者No。这个问题比原问题更复杂些,需要在任何时候回答提问的两个人的关系,并且对于信息提示还要能立即合并两个连通块。采用连通图思想显然在实现上就有所困难,因为要实时地表示人与人之间的关系。并查集及其应用用集合的思路,对于每个人建立一个集合,开始的时候集合元素是这个人本身,表示开始时不知道任何人是他的亲戚。以后每次给出一个亲戚关系时,就将两个集合合并。这样实时地得到了在当前状态下的集合关系。如果有提问,即在当前得到的结果中看两元素是否属于同一集合。对于样例数据的解释如下图:并查集及其应用输入关系分离集合初始状态{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}(2,4){1}{2,4}{3}{5}{6}{7}{8}{9}{10}(5,7){1}{2,4}{3}{5,7}{6}{8}{9}{10}(1,3){1,3}{2,4}{5,7}{6}{8}{9}{10}(8,9){1,3}{2,4}{5,7}{6}{8,9}{10}(1,2){1,2,3,4}{5,7}{6}{8,9}{10}(5,6){1,2,3,4}{5,6,7}{8,9}{10}(2,3){1,2,3,4}{5,6,7}{8,9}{10}并查集及其应用用集合的思路,对于每个人建立一个集合,开始的时候集合元素是这个人本身,表示开始时不知道任何人是他的亲戚。以后每次给出一个亲戚关系时,就将两个集合合并。这样实时地得到了在当前状态下的集合关系。如果有提问,即在当前得到的结果中看两元素是否属于同一集合。对于样例数据的解释如下图:由上图可以看出,操作是在集合的基础上进行的,没有必要保存所有的边,而且每一步得到的划分方式是动态的。如何来实现以上的算法思想呢?我们就用到并查集。并查集及其应用二、并查集的基本思想1、什么叫并查集并查集(union-findset)是一种用于分离集合操作的抽象数据类型。它所处理的是“集合”之间的关系,即动态地维护和处理集合元素之间复杂的关系,当给出两个元素的一个无序对(a,b)时,需要快速“合并”a和b分别所在的集合,这其间需要反复“查找”某元素所在的集合。“并”、“查”和“集”三字由此而来。在这种数据类型中,n个不同的元素被分为若干组。每组是一个集合,这种集合叫做分离集合(disjointset)。并查集支持查找一个元素所属的集合以及两个元素各自所属的集合的合并。并查集及其应用二、并查集的基本思想例如,有这样的问题:初始时n个元素分属不同的n个集合,通过不断的给出元素间的联系,要求实时的统计元素间的关系(是否存在直接或间接的联系)。这时就有了并查集的用武之地了。元素间是否有联系,只要判断两个元素是否属于同一个集合;而给出元素间的联系,建立这种联系,则只需合并两个元素各自所属的集合。这些操作都是并查集所提供的。并查集本身不具有结构,必须借助一定的数据结构以得到支持和实现。数据结构的选择是一个重要的环节,选择不同的数据结构可能会在查找和合并的操作效率上有很大的差别,但操作实现都比较简单高效。并查集的数据结构实现方法很多,一般用的比较多的是,数组实现、链表实现和树实现。并查集及其应用二、并查集的基本思想并查集的数据结构记录了一组分离的动态集合S={S1,S2,…,Sk}。每个集合通过一个“代表”加以识别,代表即该元素中的某个元素,哪一个成员被选做代表是无所谓的,重要的是:如果求某一动态集合的代表两次,且在两次请求间不修改集合,则两次得到的答案应该是相同的。动态集合中的每一元素是由一个对象来表示的,设x表示一个对象,并查集的实现需要支持如下操作:2、并查集支持的操作并查集及其应用二、并查集的基本思想2、并查集支持的操作MAKE-SET(x):建立一个新的集合,其仅有的成员(同时就是代表)是x。由于各集合是分离的,要求x没有在其它集合中出现过。UNION(x,y):将包含x和y的动态集合(例如Sx和Sy)合并为一个新的集合,假定在此操作前这两个集合是分离的。结果的集合代表是Sx∪Sy的某个成员。一般来说,在不同的实现中通常都以Sx或者Sy的代表作为新集合的代表。此后,由新的集合S代替了原来的Sx和Sy。FIND-SET(x):返回一个指向包含x的集合的代表。并查集及其应用三、并查集的实现及优化1、并查集的数组实现实现并查集的最简单的方法是用数组记录每个元素所属的集合的编号。查找元素所属的集合时,只需读出数组中记录的该元素所属集合的编号即可,时间复杂度为O(1)。合并两元素各自所属的集合时,需要将数组中属于其中一个集合的元素所对应的数组元素值全部改为另一个集合的编号值,时间复杂度为O(n)。由于实现简单,所以实际使用的很多。以上的数组实现虽然很方便,但是合并的代价太大。在最坏情况下,所有集合合并成一个集合的总代价可以达到O(n2)。并查集及其应用三、并查集的实现及优化2、并查集的链表实现我们需要用一定的数据结构来组织动态的集合,同一集合中的所有元素应该是联系在一起的。一种比较简单的想法是用链表来实现,每个集合对应一个链表,它有一个表头,每一个元素有一个指针指向表头,表明了它所属的类,另有一个指针指向它的下一个元素,同时为了方便实现,再设一个指针last表示链表中的最后一个元素(表尾)。可以选择静态数组(一般来说这种问题处理的对象都是连续整数,可以用下标来对应元素)来实现,定义一个记录为:typenode=recordhead,next,last:integer;end;varS:array[1..maxn]ofnode;并查集及其应用三、并查集的实现及优化2、并查集的链表实现可以得到MAKE-SET和FIND-SET的实现为:MAKE-SET(x){S[x].head=x;S[x].next=0;}FIND-SET(x){returnS[x].head}两个过程的时间复杂度都为O(1)。注意到我们采用链表以后,当有两个元素(x,y),FIND-SET(x)≠FIND-SET(y)时,两者对应不同的集合,需要将两个链表合并,做法是将一个表的表头直接接到另一个表的表尾,这步操作很简单,但势必造成修改后需要把接上去的那个表的所有head值修改,这需要线性的赋值操作,其复杂度与选择接在尾部的链表长度成正比。并查集及其应用三、并查集的实现及优化2、并查集的链表实现现在讨论UNION(x,y)的实现,假设UNION(x,y)的参数是有序的,即把y属于的集合合并到x的集合。有两种实现方法:(1)简单实现不考虑任何因素,出现FIND-SET(x)≠FIND-SET(y)时,直接将y的表头接到x的尾,同时将y中所在集合元素所有元素的head设为FIND-SET(x)。同时x的表尾也应该设为原y的表尾。注意last指针其实只要在表头结点中记录即可,因为每一次查到FIND-SET(x)都可以得到表头元素。而链表中其他元素重新记录last是无意义的。考虑输入数据的特殊性,我们总是把y接到x里去,那么如果y所在的集合非常大,每次赋值的代价就会非常高,比如出现输入为:(2,1),(3,1),(4,1),(5,1),(6,1),…,(n,1)显然y所在的集合越来越庞大,对于这种方法最坏情况下时间复杂度为O(n^2)。并查集及其应用三、并查集的实现及优化2、并查集的链表实现(2)快速实现上述简单实现非常不理想,针对y可能比较大的这个问题,可以很快产生一个聪明的想法:不妨比较x和y所在集合的大小,从而作出选择,把较短的链表接在较长的尾部,这样效果是一样的,但耗费肯定不比原来差。这就是快速实现的思路。可以在node里多设一个域number,用来记录此条链表中成员的个数。显然number记录在表头元素中即可,将两表合并的时候,只要将表的number相加,因此维护起来是非常方便的。这种快速实现的方法可以称为加权启发式合并,这里的权就是指所记录的number。并查集及其应用三、并查集的实现及优化2、并查集的链表实现可以证明这种方法的效率。当有n个元素时,在UNION上的花费(即重新赋值的次数)的上界是O(nlog2n)。考虑一个固定的对象x,当x的代表指针(head)被更新时,x必是属于一个较小的集合,因此,x的代表指针第一次更新后,结果集合必然至少有2个元素,类似的,下一次更新后,x所在的集合至少有4个元素。继续下去,可以发现,x的代表指针最多被更新log2n次,因为当x所在集合元素已经等于n以后,不可能再发生UNION操作。所以,总共有n个元素时,操作的总次数不超过nlog2n次。这就保证了整个算法的复杂度是理想的。并查集及其应用三、并查集的实现及优化2、并查集的链表实现合并两个集合时的实现过程如下:UNION(x,y){x=FIND-SET(x);y=FIND-SET(y);ifx.numbery.numberthenUNION(x,y)elseUNION(y,x);}并查集的链表实现是一种非常容易接受的算法,并且它的效率也是令人满意的。其实它的思路和数组完全一样,所以实际使用较少。并查集及其应用三、并查集的实现及优化3、并查集的树实现实现并查集的另一种方法是利用树。我们用有根树来表示集合,树中的每个节点包含集合的一个成员,每棵树表示一个集合。多个集合形成森林态,以每棵树的树根作为集合的代表,并且根结点的父结点指向其自身,树上的其他结点都用一个父指针表示它的附属关系。注意:在同一棵树中的结点属于同一个集合,虽然它们在树中存在父子结点关系,但并不意味着它们之间存在从属关系。树的指针起的只是联系集合中元素的作用。在并查集中,每个分离集合对应的一棵树,称为分离集
本文标题:并查集及其应用
链接地址:https://www.777doc.com/doc-3606887 .html