这篇文章中我来写写图的最小生成树,以及计算一个图的最小生成树的算法,即Kruskal算法和Prim算法。两个算法均使用了贪婪策略。
先来谈谈 最小生成树(Minimum Spanning Tree,MST) 的概念。这个概念分为三个部分:最小,生成,和树。因此,这里分别对这三个概念作出解释。首先,树的定义在算法导论随笔(四):树形结构与二叉树已经有完整介绍。这里,介绍生成树(Spanning tree)的概念。
首先,当我们以 “生成” 作为前缀来修饰一个图或一棵树时,表示的是该树和该图是依赖一个图产生的。对于一个图G来说,它的 “生成”子图P或“生成”树T 指的是,该子图P或树T 必须包含G中的所有顶点 ,而不必包含G中所有的边。
因此,对于生成子图和生成树,可以进行如下定义:
若一个图G的子图P包含了所有G中的顶点,则P是G的生成子图。
若G的生成子图P本身是一棵树,则P是G的生成树。
例如对于上面的图G,有下面三个生成树。
如果树T是图G的生成树,而且T中包含的边的权重之和小于等于图G的任意其他生成树,则称树T为图G的最小生成树。
例如下图中,所有顶点和所有边(包括红色边和蓝色虚线边)构成了一个图。而图中所有顶点和所有红色边构成了一棵树,这棵树就是这个图的最小生成树。
可以看出,最小生成树的实质就是用图中权重最小的边连接所有的顶点,并且保证图的连通性。当然,由于是树,因此也一定不能有循环路径(cycle)存在。
最小生成树的应用范围非常广泛,比如通讯网络、交通网络等等。
Kruskal算法使用了贪心策略来计算最小生成树。先来看算法的伪代码。
算法的输入是一个连通图G,该图有n个顶点和m条边。
算法输出一个图G的最小生成树。
这个算法先为G中的每一个顶点v创建一个集群,用通俗的方式就是,为每一个顶点创造一个 “群聊”,该群聊初始时只有一个顶点。
接下来用一个优先级队列Q存储图G中的所有边,排序方式是按照边的权重从小到大排列。
接着初始化一个空树T,作为算法的结果。
由于最小生成树T的目的是以最小权重和保证树的连通性,而越少的边就代表着越少的权重和。对于n个顶点的图,只需要保证该树T中有n-1条边,即可连接所有顶点。(5跟手指只有4个指缝)
因此,当树T中有少于n-1条边时:
从Q中取出权值最小的边,记作(u, v)。若顶点u和顶点v不处于同一个“群聊”中,则把边(u, v)存入树T中,并且把顶点u和顶点v的两个群聊合并为一个,也就是说,新群聊中有u和v两个顶点。
重复上述操作直至树T不少于n-1条边。然后树T即是图G的最小生成树。
来看下面的例子。图中共有9个顶点,因此最小生成树只需要有8条边。注意看每一步中取出的边的权值。
第一步:首先取出权重最小,即权值为1的边(h, g)。由于h和g分属于不同的群聊,因此把它们合并为到一个群中。新群为(h, g)。将(h, g)加入T中。T = {(h, g)}。如下图。
第二步:取出上图中尚未取出的边中拥有最小权值的边,即权值为2的(i, c),把它们合并为到一个群中。新群为(i, c)。T = {(h, g), (i, c)}。如下图。
第三步:取出上图中尚未取出的边中拥有最小权值的边,即权值为2的(g, f)。把(f)和(g, h)合并为到一个群中。新群为(h, g, f)。T = {(h, g), (i, c), (g, f)}。如下图。
第四步:取出上图中尚未取出的边中拥有最小权值的边,即权值为4的(a, b)。把它们合并为到一个群中。新群为(a, b)。T = {(a, b), (h, g), (i, c), (g, f)}。如下图。
第五步::取出上图中尚未取出的边中拥有最小权值的边,即权值为4的(c, f)。把(i, c)和(h, g, f)合并为到一个群中。新群为(i, c, h, g, f)。T = {(c, f), (a, b), (h, g), (i, c), (g, f)}。如下图。
第六步:取出上图中尚未取出的边中拥有最小权值的边,即权值为6的(i, g)。i和g处于同一个群(i, c, h, g, f)中,因此不做任何处理。如下图。
第七步:取出上图中尚未取出的边中拥有最小权值的边,即权值为7的(c, d)。把d和合并到c的群中。新群为(i, c, d, h, g, f)。T = {(c, d), (c, f), (a, b), (h, g), (i, c), (g, f)}。如下图。
第八步:取出上图中尚未取出的边中拥有最小权值的边,即权值为7的(i, h)。i和h处于同一个群(i, c, d, h, g, f)中,因此不做任何处理。如下图。
第九步:取出上图中尚未取出的边中拥有最小权值的边,即权值为8的(a, h)。把(a, b)和(i, c, d, h, g, f)合并。新群为(a, b, i, c, d, h, g, f)。T = {(a, h), (c, d), (c, f), (a, b), (h, g), (i, c), (g, f)}。如下图。
第十步:取出上图中尚未取出的边中拥有最小权值的边,即权值为8的(b, c)。它们处于同一个群(a, b, i, c, d, h, g, f)中,因此不做任何处理。如下图。
第十一步:取出上图中尚未取出的边中拥有最小权值的边,即权值为9的(d, e)。把(e)和(a, b, i, c, d, h, g, f)合并。新群为(a, b, e, i, c, d, h, g, f)。T = {(d, e), (a, h), (c, d), (c, f), (a, b), (h, g), (i, c), (g, f)}。如下图。
第十二步:取出上图中尚未取出的边中拥有最小权值的边,即权值为10的(f, e)。它们处于同一个群(a, b, e, i, c, d, h, g, f)中,因此不做任何处理。如下图。
第十三步:取出上图中尚未取出的边中拥有最小权值的边,即权值为11的(b, h)。它们处于同一个群(a, b, e, i, c, d, h, g, f)中,因此不做任何处理。如下图。
第十四步:取出上图中尚未取出的边中拥有最小权值的边,即权值为14的(d, f)。它们处于同一个群(a, b, e, i, c, d, h, g, f)中,因此不做任何处理。如下图。
此时T = {(d, e), (a, h), (c, d), (c, f), (a, b), (h, g), (i, c), (g, f)} 已经有了8条边,因此算法结束,T即为该图的最小生成树。也就是上图中用深黑色描出来的边。
该算法的复杂度为
其中E代表图G中边的数量,V代表G中顶点的数量。
下面来简单谈谈计算最小生成树的另一个算法:Prim算法。该算法同样使用了贪婪策略。算法的中心思想是:首先从图G中任意一个顶点开始。假设这个顶点叫A。将A加入一个群。然后比较群外所有与这个群中的顶点相邻的边,将权重最小的边的另外一个端点加入群中。然后重复上述操作直到所有顶点都已被加入到群中。
我们来看下面的例子。假设我们有下面的一个图 ,算法从顶点a开始。
将顶点a加入群中。上图中群外与群内任意一点连接的边有(a, b)和(a, h),其中(a, b)的权值最小,因此把b加入群中。如下图。
接下来,上图中与群中的顶点连接的边为(b, c)和(a, h),两个边的权值都为8。这里我们随机选择(b, c)这条边,把c加入群中。如下图。
接下来,上图中与群内顶点连接的权重最小的边为权值为2的(c, i)。将i加入群中。如下图。
接下来,上图中与群内顶点连接的权重最小的边为权值为4的(c, f)。将f加入群中。如下图。
接下来,上图中与群内顶点连接的权重最小的边为权值为2的(f, g)。将g加入群中。如下图。
接下来,上图中与群内顶点连接的权重最小的边为权值为1的(g, h)。将h加入群中。如下图。
接下来,上图中与群内顶点连接的权重最小的边为权值为7的(c, d)。将d加入群中。如下图。
接下来,上图中与群内顶点连接的权重最小的边为权值为9的(d, e)。将e加入群中。如下图。
此时群中已经包含所有顶点。算法结束。这个群就是最小生成树,也就是上图中用深黑色描出来的边。
算法的伪代码如下。
算法的复杂度t同样为
本篇文章中讨论了最小生成树的两种算法:Kruskal算法和Prim算法。两种算法均使用贪心策略。后面的文章中,我会介绍更多的图算法,例如同样使用贪心策略求最大流最小切的Ford-Fulkerson算法等。