最小生成树的定义

假定有一个连通无向图[算法导论笔记]--最小生成树,其中[算法导论笔记]--最小生成树是节点集合,[算法导论笔记]--最小生成树表示节点间的边的集合,对于每条边[算法导论笔记]--最小生成树,具有权重[算法导论笔记]--最小生成树.这里希望找到一个无环子集[算法导论笔记]--最小生成树,既能够将所有节点连接起来,又具有最小的权重和。我们就称之为一个最小生成树

解决最小生成树问题的算法,常见的为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的证明(摘自《算法导论》)

[算法导论笔记]--最小生成树

 

 

 

 

 

 

 

 

 

相关文章: