最小生成树的定义
假定有一个连通无向图,其中
是节点集合,
表示节点间的边的集合,对于每条边
,具有权重
.这里希望找到一个无环子集
,既能够将所有节点连接起来,又具有最小的权重和。我们就称之为一个最小生成树。
解决最小生成树问题的算法,常见的为Kruskal算法和Prim算法。如果使用普通的二叉堆,那么可以很容易地把这两个算法的时间复杂度限制在O(ElogV)数量级内。但是如果使用斐波那契堆,则Prim算法的运行时间将改善为O(E+logV),此运行时间在|V|远小于|E|的情况下较二叉堆有较大改进。
一、最小生成树的形成
假定有一个无向连通图和权重函数
,若希望找出
的一个最小生成树,先前所提及的两种算法都采取了贪心策略。
这个贪心策略可以用以下方式来描述。该方式在每个时刻生长最小生成树的一条边,并且在整个策略的实行过程中,做出如下保证:
在每遍循环前,A是某棵最小生成树的一个子集。
在每一步,我们要做的事就是选择一条边,让其加入到集合
中,使得
不违反循环不变式,即
也是某棵最小生成树的子集。由于可以安全地将这种边加入到集合
而不会破坏
的循环不变式,因此也称这样的边为
的安全边。
为了解决最小生成树问题,这里需要做出一些定义:
无向图的一个切割
是集合V的一个划分,如果一条边
的一个端点位于
,另一个端点位于
,则称该边横跨切割
;
如果集合中不存在横跨该切割的边,则称切割尊重集合A。在横跨一个尊重集合A的切割的所有边中,权重最小的边称作轻量级边。
关于这个定义,下面图片可以很直观地反映出来(摘自《算法导论》)
二、定理
现在,有了最小生成树的概念以及定义,这里可以根据之做出一个相应的推论:
定理1:设 是一个在边E上定义了实数权重函数
的连通无向图。设集合
为
的一个子集,且
包括在图
的某棵最小生成树中,设
是
中尊重集合
的一个切割,又设
是横跨该切割的一条轻量级边,那么
对于集合
是安全的。
这段话的大意就是,对于一个尊重集合A(A是某个最小生成树的子集)的切割,如果把一条横跨切割且权重最小的边加入到集合A中,A仍然是最小生成树的一个子集(个人理解)。
额,如果你好奇于怎么证明,可以参考《算法导论》,这里不详细赘述了,但会在最后附录贴出。
由定理1,可以进一步得到新的推论:
推论2:设 是一个在边E上定义了实数权重函数
的连通无向图。设集合
为
的一个子集,且
包括在图
的某棵最小生成树中,并设
为森林
中的一个连通分量。如果边
是连接
和
中某个其他连通分量的一条轻量级边,则边
对于
而言是安全的。
推论2的证明很简单,因为切割尊重集合A,边
是横跨该切割的一条轻量级边,所以该边对于集合A而言是安全的。
三、Kruskal和Prim算法
Kruskal算法
Kruskal算法找到安全边的办法是,在所有连接森林中两棵不同树的边里,找到权重最小的边,设
和
是
所连接的两棵树,由于边
一定是连接
和其他棵树的一条轻量级边,根据推论2,边
是
的一条安全边。其伪代码如下:
该伪代码的过程如下:1~3行将集合初始化为一个空集,并且创建|V|棵树,每棵树仅包含一个结点。5~8行的for循环将会按照权重由低到高的次序遍历每一条边。对于任意边(u,v),应当检测其是否属于同一棵树,如果是的话,就不能将该边加入到最小生成树对应的森林里,否则会形成环路而破坏树的性质。第7行将该边加入到集合A中,第8行则将两棵树中的结点进行合并(变成同一棵树)。其过程示意如下图(摘自《算法导论》)
很明显可以看出来,Kruskal算法先将所有边都存起来,之后按照权重由低到高进行遍历。
Prim算法
Prim算法的工作原理和Dijkstra的最短路径算法(这个算法算是非常非常出名了吧)类似。Prim算法所具有的一个性质是集合A中的边总是构成一棵树。这棵树从任意一个根节点r开始,一直长大到覆盖V中所有节点为止。算法每一步在连接集合A和A之外的节点的所有边中,选择一条轻量级边加入到A中。因此,当算法终止时,A中的边形成一棵最小生成树。本策略也属于贪心策略,过程如下所示(摘自《算法导论》):
Prim算法的流程非常简单,其伪代码如下:
这个伪代码描述的正是Prim算法的工作流程,1~5行将每个结点的key设为∞(除根节点外,根结点的r和key都为0,以便于使之成为第一个被处理的结点),将每个结点的父结点设为NIL,对于最小队列Q进行初始化,使得其包含G中所有结点
第7行找出结点该结点是某条横跨切割
的轻量级边的端点(属于Q)。接着将
从队列
中删除,并将其加入到集合
内,也就是将边
加入到集合A内(注:(
)表示u的父结点)。
(这里解释一下第7行,一开始选取了根结点以后,就会更新Q中所有与根结点相邻的结点,那些结点原本key都是∞,随后更新为边的权重,因为边的权重肯定小于∞,所以这些结点在下次选边的时候一定是位于最小优先级队列前端的,这下,通过EXTRACT-MIN找出的肯定就是所谓的横跨切割的边了呀)
第8~11行的for循环将每个与u邻接但不在树中的结点v的key和π的属性进行更新。
Prim算法的运行时间取决于最小优先级队列Q的实现方式。如果将Q实现为一个二叉最小优先队列, Prim算法总时间代价为O(ElogV+VlogV)
如果使用斐波那契堆来实现最小优先级队列Q,Prim算法的渐近运行时间可以得到进一步改善,为O(E+VlogV)
附录:定理1的证明(摘自《算法导论》)