《算法笔记》——第十章 最小生成树 学习记录

最小生成树

最小生成树(Minimum Spanning Tree,MST)是在一个给定的无向图G(V,E)中求一棵树T,使得这棵树拥有图G中的所有顶点,且所有边都是来自图G中的边,并且满足整棵树的边权之和最小。

最小生成树有3个性质需要掌握:

  1. 最小生成树是树,因此其边数等于顶点数减1,且树内一定不会有环。
  2. 对给定的图G(V,E),其最小生成树可以不唯一,但其边权之和一定是唯一的。
  3. 由于最小生成树是在无向图上生成的,因此其根结点可以是这棵树上的任意一个结点。于是,如果题目中涉及最小生成树本身的输出,为了让最小生成树唯一,一般都会直接给出根结点,读者只需以给出的结点作为根结点来求解最小生成树即可。

求解最小生成树一般有两种算法,即prim算法与kruskal 算法。这两个算法都是采用了贪心法的思想,只是贪心的策略不太一样。

prim算法(读者可以将其读作“普里姆算法”)用来解决最小生成树问题,其基本思想是对图G(V,E)设置集合S,存放已被访问的顶点,然后每次从集合V-S中选择与集合S的最短距离最小的一个顶点(记为u),访问并加入集合S。

之后,令顶点u为中介点,优化所有从u能到达的顶点v与集合S之间的最短距离。这样的操作执行n次(n为顶点个数),直到集合S已包含所有顶点。可以发现,prim算法的思想与最短路径中Dijkstra算法的思想几乎完全相同,只是在涉及最短距离时使用了集合S代替Dijkstra算法中的起点s。

prim算法的基本思想是对图G(V,E)设置集合S来存放已被访问的顶点,然后执行n次下面的两个步骤(n为顶点个数):

  1. 每次从集合V-S中选择与集合S最近的一个顶点(记为u),访问u并将其加入集合S,同时把这条离集合S最近的边加入最小生成树中。
  2. 令顶点u作为集合S与集合V-S连接的接口,优化从u能到达的未访问顶点v与集合S的最短距离。

可以发现,prim算法与Dijkstra算法使用的思想几乎完全相同,只有在数组d[]的含义上有所区别。其中,Dijkstra算法的数组d[]含义为起点s到达顶点Vi的最短距离,而prim算法的数组d[]含义为顶点Vi与集合S的最短距离,两者的区别仅在于最短距离是顶点Vi针对“起点s”还是“集合S”。另外,对最小生成树问题而言,如果仅是求最小边权之和,那么在prim算法中就可以随意指定一个顶点为初始点,例如默认使用0号顶点为初始点。

Dijkstra算法和prim算法只有优化d[v]的部分不同,而其他语句都是相同的。这再次说明:Dijkstra算法和prim算法实际上是相同的思路,只不过是数组d[]的含义不同罢了。

kruskal算法

kruskal算法(读者可以将其读作“克鲁斯卡尔算法”)同样是解决最小生成树问题的一个算法。和prim算法不同,kruskal算法采用了边贪心的策略,其思想极其简洁,理解难度比prim算法要低很多。

kruskal算法的基本思想为:在初始状态时隐去图中的所有边,这样图中每个顶点都自成一个连通块。之后执行下面的步骤:

  1. 对所有边按边权从小到大进行排序。
  2. 按边权从小到大测试所有边,如果当前测试边所连接的两个顶点不在同一个连通块中,则把这条测试边加入当前最小生成树中;否则,将边舍弃。
  3. 执行步骤②,直到最小生成树中的边数等于总顶点数减1或是测试完所有边时结束。而当结束时如果最小生成树的边数小于总顶点数减1,说明该图不连通。

①如何判断测试边的两个端点是否在不同的连通块中。
②如何将测试边加入最小生成树中。
事实上,对这两个问题,可以换一个角度来想。如果把每个连通块当作一个集合,那么就可以把问题转换为判断两个端点是否在同一个集合中,而这个问题在前面讨论过一对,就是并查集。并查集可以通过查询两个结点所在集合的根结点是否相同来判断它们是否在同一个集合,而合并功能恰好可以把上面提到的第二个细节解决,即只要把测试边的两个端点所在集合合并,就能达到将边加入最小生成树的效果。

另外,假设题目中顶点编号的范围是[1,n],因此在并查集初始化时范围不能弄错。如果下标从0开始,则整个代码中也只需要修改并查集初始化的部分即可。

可以看到,kruskal算法的时间复杂度主要来源于对边进行排序,因此其时间复杂度是\(O(ElogE)\),其中E为图的边数。显然kruskal适合顶点数较多、边数较少的情况,这和prim算法恰好相反。

于是可以根据题目所给的数据范围来选择合适的算法,即如果是稠密图(边多),则用prim算法;如果是稀疏图(边少),则用kruskal算法。

posted @ 2021-03-01 14:15  Dazzling!  阅读(59)  评论(0编辑  收藏  举报