您好,欢迎访问三七文档
当前位置:首页 > 商业/管理/HR > 管理学资料 > 求无向图的最小生成树算法——Prim与Kruskal
一.Prim算法1.算法思想对于图G=(V,E),用Prim算法求最小生成树T=(S,TE)的流程如下①初始化:设S、TE为空集,任选节点K加入S。②选取一条权值最小的边(X,Y),其中X∈S,且not(Y∈S)即,选取一条权值最小的、连接着S中一点与S外一点的边。将Y加入S中,边(X,Y)加入TE中重复②直到V=S即所有G中的点都在S中,此时的T为G的最小生成树。由此流程可见,Prim算法求最小生成树时任何时候的T都是一颗树。2.实现显然,Prim算法的主要运行时间花在过程②的选边中。看起来复杂度是O(VE)=O(V^3)不是么,效率也太低了吧……为了比较快速地选边,我们用两个数组lowcost、closest动态地维护每一个点到S的最短距离。在某一状态下,lowcost[i]表示所有与i相连且另一端点在S中的边中的权值最小值,closest[i]表示在S中且与i相连的点中与i之间距离最小的点。显然,lowcost[i]=w(i,closest[i])。需要注意的是两个数组记录的都是边而不是路径。若i没有边直接连向S,则lowcost[i]=∞。另外,若i已在S中,则lowcost[i]=0。设出发点为x。初始时对于任意k∈V,closest[k]=x,lowcost[k]=w(k,x)【w(i,j)表示i、j间的距离。初始化时,若两点间没有边则w(i,j)赋为一个足够大的整数(如maxint),并且所有点到自身的距离赋为0,即w(i,i)=0】每一次找出lowcost中不为0的最小值lowcost[i],然后把i加入S(即lowcost[i]:=0),然后对于图中所有点k,若w(k,i)lowcost[k],则把lowcost[k]赋为w(k,i),把closest[k]赋为i。【由于S中所有点的lowcost都为0,所以只影响到S以外的点】以上操作重复|V|-1次结束。由于每次加入S的点i都在当时取到了符合流程②的边min{lowcost},而lowcost[i]=w(i,closest[i]),所以此时的最小生成树的各边就是(i,closest[i]),i∈V且not(i=x)【需要注意的是出发点x的closest[x]还是x,所以应忽略,实际取到x-1条边】。把i从1取到|V|,便得到最小生成树T的每条边。程序:for(k=1;k=n;k++){//x为S中的第一个点,x任选一个都可lowcost[k]=w[x][k];//lowcost表示k点到前集合的最小值,是k与前集合中的closet[k]间的距离closest[k]=x;}//初始化for(i=2;i=n;i++){//除去x,还要加入n–1个点temp=maxint;for(j=1;j=n;j++)if(lowcost[j]temp&&lowcost[j]!=0){temp=lowcost[j];k=j;}//找出S外距S最近的点klowcost[k]=0;//将k加入生成树for(j=1;j=n;j++)if(w[k][j]lowcost[j]){lowcost[j]=w[k][j];closest[j]=k;}//由新加入S中的k点使某些点到S的距离缩短,所以更新各点的lowcost和closest,即再次初始化}for(i=1;i=n;i++)if(i!=closest[i]){printf(“%d%d”,i,closest[i]);}//输出最小生成树的各边不难看出,以上算法包含一个二重循环,算法复杂度为O(V^2),与图的稠密度无关。【Prim算法与Dijkstra算法比较】:都是求最小问题,prim算法用来求最小生成树,Dijkstra算法用来解最短路径问题。两者都可分为三步骤:先初始化,再找到路径最小点标记后加入到前面的集合中,最后更新数据。就这三步不断循环直到后面的集合为空。所不同的是:1).前者是求如何将散点连起来最后总路程最小,而后者是求其他点到一固定源的最短距离分别是多少。2).前者初始化时任取一点x放入前集合,使各点到前集合的最小值为w[x][k],此时前集合中使该最小值成立的就是点x。后者初始化时要先确定源,将源i放入前集合,再将各点到源的最短距离定为dist[i][k]。3).前者加入点时,是将到前集合距离最短的点加入并标记。后者是将到源最近的点加入并标记。4).更新时前者由于前集合加入了点使得某些点到新加入的这个点的距离成为到前集合的最短距离所以更新,并记下这个最短距离是和前集合里这个新点间产生的。后者是由于某些点可以以新加入的点为中间点使得到源的距离变短所以要更新。3.使用堆优化如果我们把要取的边作为一个最小优先级队列,用堆来选取边与调整,则算法可以得到优化。对于上面代码绿色部分,转换成堆后就是一个建立一个有V个节点的小根堆的过程,复杂度为O(V);对于红色部分,就是取堆顶元素【O(1)】并删除、维护【O(logV)】;对于橙色部分,对于for循环我们可以用一个数组edge[i]记录与i节点相连的所有边,这样我们就把for循环的复杂度变成了与E相关。显然,图中的每条边仅在其两顶点进入最小生成树时被访问一次,也就是每条边被访问2次,因此橙色部分的执行次数总共是2E次而与外面的while无关。里面的lowcost修改时需要维护堆,复杂度为O(logV)。因此,算法的总复杂度为O(VlogV+ElogV)=O(ElogV)。具体程序见本文最下方。对于|E|接近|V|的稀疏图,算法复杂度接近O(VlogV)显然比O(V^2)的算法优秀很多。然而对于E接近N^2的稠密图,算法复杂度接近O(V^2logV)反而不如平方算法。因此,使用堆优化的Prim算法适用于稀疏图,而不优化的Prim算法适用于稠密图。不过O(V^2logV)实际上不比O(V^2)慢多少,而现在的题目大多是稀疏图,所以这个算法在总体上是优于朴素Prim算法的。顺便一提,如果用斐波那契堆来做的话,时间复杂度可以进一步优化为O(E+VlogV)。但事实上这样做的代码长度会到令人发指的地步,所以是一种只存在于论文的算法。应付一般的竞赛用堆优化的Prim就可以应付所有数据了。二.Kruskal算法1.算法思想设最小生成树为T=(V,TE),初始时设TE为空集。将G中所有边按权值排序,从小到大选取边。若当前选取的边加入TE后不会使TE出现环,则将边加入TE,否则舍弃之。当TE中有n-1条边时,算法结束。此时T=(V,TE)为G的最小生成树。关于判断是否会产生环,有一个简单的方法:若i与j中已有一条路径i-k1-k2-…-kn-j,若加入边(i,j)就会形成环【i-k1-k2-…-kn-j-i】。因此,若一条边连接两个连通分量就不会形成环,反之则会形成环。初始时每一个点就是一个连通分量,当加入(i,j)时,判断i与j是否属于不同连通分量。如果是的话,在加入边后把i与j所在的连通分量合并。重复以上操作,就可以保证不会产生环。2.实现显然,Kruskal的难点在于判断、合并连通分量。Pascal语言中已有集合类型可以实现这一操作,不过竞赛中set是一个极其危险的类型【基本常识】。此处只用到查询、合并两个操作,是一个很明显的并查集模型,每一个连通分量就是并查集里的一个集合,通过并查集的算法即可实现算法。3.时间复杂度分析对E条边排序的复杂度为O(ElogE);E次并查集的查找、合并的复杂度也约为O(ElogE),因此算法的复杂度为O(ElogE)。又由于|E|的取值范围为【|V|-1】~【|V|(|V|-1)/2】=【|V|】~【|V|^2】,所以log|E|的范围是【log|V|】~【2log|V|】。因此O(logE)可以写作O(logV)。为了方便与Prim算法复杂度的比较,一般来说把Kruskal的复杂度表述为O(ElogV)
本文标题:求无向图的最小生成树算法——Prim与Kruskal
链接地址:https://www.777doc.com/doc-5690431 .html