Chinaunix首页 | 论坛 | 博客
  • 博客访问: 43475
  • 博文数量: 11
  • 博客积分: 203
  • 博客等级: 入伍新兵
  • 技术积分: 201
  • 用 户 组: 普通用户
  • 注册时间: 2012-08-05 22:34
文章分类
文章存档

2014年(1)

2013年(3)

2012年(7)

我的朋友

分类: C/C++

2012-09-06 22:36:15

Minimum Spanning Tree, MST)是图论的一个分支,主要用于从一个包含n个结点的连通图中提取出一个包含全部n个结点的最小连通子图。《算法导论》中给出的术语表示是:对于无向连通图G=(V,E),其中V是顶点集合,E是连接各个顶点的边的集合。对图中每一条边(u,v)E,都有一个权值w(u,v)表示连接uv的代价。我们希望找出一个无回路的子集TE,它连接了所有的顶点,其权值之和w(T)=w(u,v)为最小。

解决最小生成树问题有两种主流的算法,分别称作普里姆(Prim)算法和克鲁斯卡尔(Kruskal)算法。不过,究竟何时用哪一种算法令很多人纠结,本文就来详细探讨这一问题。

先来说说两种算法的共同点,易知n个顶点的无向连通图最小生成树一定由全部顶点及条边组成,而寻找这些顶点和边的过程是一种贪心策略:维护一个最小生成树子集A,使得每次都将G中的一条边加入A中,直到A满足最小生成树的条件为止。就Prim算法而言,每次加入A中的是(G-A)A中具有最小权值相连接的那条边(相当于把顶点按(G-A)A分为两个阵营);而对于Kruskal算法,每次加入A中的是(G-A)中具有最小权值且两个顶点在A中互不可达的边。换句话说,Prim算法每次选择是基于顶点的,而Kruskal算法每次选择是基于边的。

Prim算法的基本思想是:从任意的顶点开始,构造所求的最小生成树子集A;每次都以(G-A)中的顶点为对象,找到(G-A) A中顶点对所形成的边的权值最小者,将该条边加入A中,直到A中包含了所有的顶点为止。Prim算法的时间复杂度取决于实现A的数据结构,如果采用基于二叉堆的优先队列来实现的话,那么得到最小关键字、添加删除元素都可以在logV时间内来完成,外围循环的次数是(V-1),不过每次更新权值是对图的邻接表进行扫描,整体的复杂度是O(E),考虑到V<=E+1,因此整个Prim算法的时间复杂度为O(ElogV)

算法的基本思想是:首先对所有的边的权值进行从小到大排序,然后开始构造所求的最小生成树子集A;每次都以(G-A)中的边为对象,根据之前的排序结果找到(G-A) 中权值最小且两顶点在A中互不可达的边,将该条边加入A中,直到A中包含了(n-1)条边为止。Kruskal算法的时间复杂度取决于实现A的数据结构及排序算法。如果采用快速排序加并查集来实现的话,排序的时间复杂度为O(ElogE) ,创建集合的时间复杂度为O(V),元素查找及集合合并的时间复杂度为O(E),仍然考虑到V-1<=E,因此整个Kruskal算法的时间复杂度为O(ElogE)O(ElogV)

可以看出,从渐近的角度来说,两种算法的时间复杂度是相同的。不过,根据我对实践中的观察和总结,80%以上的最小生成树问题都是用Prim算法解决的。其实这一现象是有一定原因的。首先,虽说渐近意义上两种算法的计算时间相同,但实际操作中,Prim算法要更快一些。这是因为Kruskal算法单单排序一项就要用掉O(ElogE)的时间,而这个时间几乎相当于算法耗时的全部(如果Prim算法采用合适的数据结构来实现的话)。即便是快速排序,也仅仅是平均意义上的O(ElogE),某些数据依然可能使时间复杂度退化为O(E^2)。这一点对于稠密图尤其不利,因为待排序的边数太多,往往严重拖慢程序的运行,而我们甚至还没考虑对并查集操作的时间。其次,Kruskal算法的空间开销往往较大。如果顶点数V>1500,对于稠密图,维护一个长度为EV^2/2=10^6的边权值数组可能就会让系统无法负荷,另外我们还没考虑并查集这个长度为V的数组;而Prim算法无需对边操作,只要两个长度为V的数组存储父亲结点及最小权值就行了。这种空间复杂度的差异往往使得解决问题的人在面对顶点数较多的稠密图时除了Prim算法外别无选择。最后,Prim算法是基于顶点的,每次循环结束得到的也是一个最小生成树,是对于已经加入到集合A中的结点而言的最小生成树。这种满足子问题解决方案的性质在某些情况下很有优势。而Kruskal算法是基于边的,每次循环结束后得到的是一个最小生成子森林,这种结构对于解决类似问题的用处要比Prim算法中的子问题差很多,因此这方面Kruskal算法也是逊色的。

上面我们多次提到稠密图,这样是不是局限了问题的范围呢?实际上还真不是。图这种数据结构在现实应用中的实例包括网络布局、城市规划等,这些实例中的图往往都是稠密图,稀疏图少之又少;试想一下,V个结点形成的图边的个数往往与V^2数量级相同,如果边数很少一般有一些结点就是多余的,因此实际使用中我们大多数用的就是稠密图,我们做的假设没有问题。那么,是不是Kruskal算法就真的无用武之地呢?也不一定,看看下面的问题。

问题:现在要为N个村庄铺路,使得任意两个村庄之间可以互相到达(可间接到达),现在某些村庄之间已经铺好了道路,问如何将剩余的道路铺好使得最终全部道路的长度最短?

对于这个问题,不经思索就想使用Prim算法是鲁莽的,例如截取Prim算法中循环的部分:题目中有些边已经被选进A中,那么就沿着Prim算法中的N-1次循环继续下去,直到所有顶点都在A中。可是,这种算法忽略了一个问题:Prim算法每次循环后A中都是一颗树,可是该算法能满足要求吗?实际上从算法的一开始就不能满足,因为铺好的那些路不一定形成一棵树,很可能是森林!另外即便是树也不一定是真正的最小生成树的子集;这个问题本身是最小生成树的变形,其实现方式已经沿着Kruskal算法的思想了,因此我们不得不继续做下去,即针对所有的边进行排序,每次选取权值最小且不属于已经修好的路段且两顶点在A中互不可达的边,然后直到(N-1)条边被选入A中为止,这才是正确的算法。

看来,Kruskal算法在某些情况也是有其作用的,不过这种特例情况还是很少,只要做出细致的分析一般都能鉴别出来。作为本文的结语,笔者针对最小生成树使用哪种算法提出几点建议:

1.对于顶点数较多的图Prim算法,慎用Kruskal算法,因为后者可能会造成内存溢出;

2.对于稠密图,建议考虑Prim算法,慎用Kruskal算法,因为后者可能消耗更多的时间;

3.对于最小生成树问题的变形,建议仔细分析,看其基本是向哪种算法的方向倾斜,然后考虑采用那种方法的可行性。

阅读(1945) | 评论(1) | 转发(0) |
给主人留下些什么吧!~~

_Rayx2012-09-07 08:59:40

本人与文中有些关点不一样:
1. Prim算法的优势是对稠密图有较好的复杂度,而prim对稀疏图有较好的复杂度。
2. 快排某些数据存在使复杂度变为O(n^2),这个其实在现实中基本是不存在的,因为快排中间加入了随机化,而且还有更多的排序最坏复杂度为O(nlogn),但是因为他们的实际表现效率不如快排,所以一般会选用快排。
3. E≈V^2/2=10^6的边权值数组可能就会让系统无法负荷
这个数据量对现在计算机来说是完全可接受的。
4. 因此实际使用中我们大多数用的就是稠密图
这个结论。。。