1. 什么是最小生成树(Minimum Spanning Trees)

  对于一个无向图,如果它的所有边都带有一定的数值(即带权),则会变成下面的样子

  

  假设这些点都是城市,每个城市之间的连线代表城市间的道路,线上的数字代表着道路的长短。当然,修越长的道路就需要越多的资源。

  那么如果要用最少的资源把所有城市都联系起来(即任意城市A能沿着道路抵达任意城市B),我们应该怎样建设道路呢?答案如下图:

  

  这就是最小生成树:用最小的权值总和(即数值总和)把所有点都联系起来。应注意到:最小生成树的边总数=此无向图的点总数-1。

  注意,最小生成树里是不应该有闭合循环的,如:

  

  权值为24的那条边显然是多余的。

2. 生成带权的无向图

  因为这种无向图只比普通无向图多了些权值,我们只需在每条边上多加一个变量来记录权值即可。

  使用的邻接列表也需要把权值信息写上,如下图:

  

  目前有两种比较主流的算法来找最小生成树:kruskal's algorithm(克鲁斯卡尔算法)和Prim's algorithm(普林演算法)。(中文译名采用音译的方法。)

  接下来,我们将逐一介绍这两种算法。

 

3. kruskal's algorithm(克鲁斯卡尔算法)

  从例子入手:

  

 

   为了容易理解,我们把所有边按权值大小排成递增的数组。实际代码操作时,只需要写个方法,让数组输出一个最小值即可。

  

  我们需要创建几个数组:

  创建一个点的数组Points,把最小生成树T的点存储起来;

  创建一个边的数组mst(Minimum spanning tree的简写)来把最小生成树的边都存储起来。

  创建一个边的数组pq把无向图里所有的边都存储起来。

  首先数组pq输出并移除一个最小值:0-7  0.16。

  由于目前的最小生成树T还没有点,所以把0和7加进Points。

  把0-7这条边加进mst。

  

  然后数组pq输出并移除一个最小值:2-3  0.17。

  检查2和3是否在Points中。如果不在,则把这两个点加入到Points中。

  把2-3这条边加进mst。

  

  然后数组pq输出并移除一个最小值:1-7  0.19。

  7已经在Points中了,只需把1加进Points里。

  把1-7这条边加进mst。

  

  然后数组pq输出并移除一个最小值:0-2  0.26。

  由于0,2都已经在Points里了,我们需要检查如果把0-2这条边加进最小生成树里是否会形成闭合循环。

  检查方法稍后介绍。

  如果不会形成闭合循环,则把这条边加进最小生成树T中;否则,则无视这条边,继续看下一条边。

  在这里,不形成闭合循环,把0-2这条边加进mst中。

  

  然后数组pq输出并移除一个最小值:5-7  0.28。

  7已经在Points中了,只需把5加进Points里。

  把5-7这条边加进mst。

  

  

  然后数组pq输出并移除一个最小值:1-3  0.29。

  由于1,3都已经在Points里了,我们需要检查如果把1-3这条边加进最小生成树里是否会形成闭合循环。

  会形成闭合循环,无视之。

  然后数组pq输出并移除一个最小值:1-5  0.32。

  由于1,5都已经在Points里了,我们需要检查如果把1-5这条边加进最小生成树里是否会形成闭合循环。

  会形成闭合循环,无视之。

  然后数组pq输出并移除一个最小值:2-7  0.34。

  2,7都已经在Points里,且会形成闭合循环,无视之。

  然后数组pq输出并移除一个最小值:4-5  0.35。

  5已经在Points中了,只需把4加进Points里。

  把5-4这条边加进mst。

  

  然后数组pq输出并移除一个最小值:2-1  0.36。

  2,1都已经在Points里,且会形成闭合循环,无视之。

  然后数组pq输出并移除一个最小值:4-7  0.37。

  4,7都已经在Points里,且会形成闭合循环,无视之。

  然后数组pq输出并移除一个最小值:4-0  0.38。

  4,0都已经在Points里,且会形成闭合循环,无视之。

  然后数组pq输出并移除一个最小值:6-2  0.4。

  2已经在Points中了,只需把6加进Points里。

  把6-2这条边加进mst。

  

  此时,mst里的边数=无向图的点总数-1。说明最小生成树已经形成了,算法结束。

  现在讨论如何检测新加入的边(v-w)是否会使最小生成树形成闭合循环。假设现在最小生成树的点总数为V。

  可以用深度优先搜索来检测v是否能抵达w,如果可以,说明最小生成树中已经有路连接v和w了,再加一条v-w会形成闭合循环。

  但是,还有一种方法更为高效:并查集算法。简单总结一下就是:

  v与和v相连的所有点形成一个区域,此区域用一个点a来代表。

  w与和w相连的所有点形成一个区域,此区域用一个点b来代表。

  然后对比a是否等于b,如果是,则说明v,w处于同一区域,最小生成树中已经有路连接v和w了,再加一条v-w会形成闭合循环;如果不是,说明v和w处于不同区域,可以加v-w进最小生成树。

  总结一下kruskal's algorithm(克鲁斯卡尔算法)的通用思路就是:

  把所有边放进一个数组里。

  然后数组输出并移除一个拥有最小权值的边,如果这条边加入到最小生成树内不会形成闭合循环,则把它加进去;否则无视它。

  如此循环,直到最小生成树的边总数=无向图的点总数-1为止。

  顺带一提:输出数值的最小值的方法可以把此数组做出最小堆的结构,然后输出第一个元素即可。

代码大概是这样子的:

  

 

4. Prim's algorithm(普林演算法)

  此算法有两种实现版本:懒惰算法版(lazy implementation)和贪心算法版(eager implementation)。

  懒惰算法和贪心算法是两种思想,贪心算法也被称为过度热情算法。

  从一个例子中感受一下:

  假设a在他的房间里愉快地玩耍,突然他妈叫他收拾房间。此时a有两个选择:要么马上收拾,要么等会再收拾。

  如果选择马上收拾,a会马上打扫房间,甚至把走廊也扫了一遍。这就是贪心算法。

  如果选择等会再收拾,a会等到他妈要来检查的前一瞬间才开始收拾。这就是懒惰算法。  

  对于一个程序来说,贪心算法就是当程序收到一个指令时,它不但会完成指令,还会过度热情地多做点其它事情;

  懒惰算法就是程序收到一个指令时,它会暂时无视之,等到我们要用到那个指令生成的结果时,程序才会去做这个指令。

  接下来,逐一介绍这两个实现版本:

 

普林演算法之懒惰实现版本

  从例子入手:

  

  我们需要创建几个数组:

  创建一个点的数组Points,把最小生成树T的点存储起来;

  创建一个边的数组mst(Minimum spanning tree的简写)来把最小生成树的边都存储起来。

  创建一个边的数组pq。

  首先,随便找一个点开始:如0.

  把0加入到Points里,把含点0的所有边加入到pq里。(为了方便理解,这里的边按权值递增排进数组,实际操作只需从数组中输出最小值,不必排序)

  

  然后pq输出并移除最小一个最小值:0-7  0.16。

  0已经在Points中了,只需把7加进Points里。

  把含点7的除了边的另一个端顶点已经在Points里之外的所有边加入到pq里。

  

  然后pq输出并移除最小一个最小值:1-7  0.19。

  7已经在Points中了,只需把1加进Points里。

  把含点1的除了边的另一个端顶点已经在Points里之外的所有边加入到pq里。

  

  然后pq输出并移除最小一个最小值:0-2  0.26。

  0已经在Points中了,只需把6加进Points里。

  把含点2的除了边的另一个端顶点已经在Points里之外的所有边加入到pq里。

  

  然后pq输出并移除最小一个最小值:2-3  0.17。

  2已经在Points中了,只需把3加进Points里。

  把含点3的除了边的另一个端顶点已经在Points里之外的所有边加入到pq里。

  

 

  然后pq输出并移除最小一个最小值:1-3  0.29。

   3,1都已经在Points里,无视之。

  然后pq输出并移除最小一个最小值:7-5  0.28。

  7已经在Points中了,只需把5加进Points里。

  把含点5的除了边的另一个端顶点已经在Points里之外的所有边加入到pq里。

  

  然后pq输出并移除最小一个最小值:1-5  0.32。

   5,1都已经在Points里,无视之。

  然后pq输出并移除最小一个最小值:7-2  0.34。

   7,2都已经在Points里,无视之。

  然后pq输出并移除最小一个最小值:4-5  0.35。

  5已经在Points中了,只需把4加进Points里。

  把含点4的除了边的另一个端顶点已经在Points里之外的所有边加入到pq里。

  

  然后pq输出并移除最小一个最小值:1-2  0.36。

   2,1都已经在Points里,无视之。

  然后pq输出并移除最小一个最小值:7-4  0.37。

   7,4都已经在Points里,无视之。

  然后pq输出并移除最小一个最小值:0-4  0.38。

   0,4都已经在Points里,无视之。

  然后pq输出并移除最小一个最小值:2-6  0.4。

  2已经在Points中了,只需把6加进Points里。

  把含点6的除了边的另一个端顶点已经在Points里之外的所有边加入到pq里。(没有可加的边)

  

  此时,mst里的边数=无向图的点总数-1。说明最小生成树已经形成了,算法结束。

  这个算法懒惰在哪呢?我们沿用那个收拾房间的例子。

  总结一下通用思路:

  1. 首先随便选一个点加进Points

  2. 然后把含此点的除了边的另一个端顶点已经在Points里之外的所有边加入到pq里。(孩子a听到要收拾房间,就把东西全部塞到房间里了)

  3. 然后pq输出并移除最小一个拥有最小值权值的边,如果此边的另一端点在Points里,则无视之;如果不在,则把此点加进Points里。(a的妈妈要来查房了,马上收拾。)

  4. 重复2,3步直到mst里的边数=无向图的点总数-1。

代码实现:

  

  

  

普林演算法之贪心实现版本

  从例子入手:

  

  我们需要创建几个数组:

  创建一个点的数组Points,把最小生成树T的点存储起来;

  创建一个边的数组mst(Minimum spanning tree的简写)来把最小生成树的边都存储起来。

  创建一个的数组pq。

  首先,随便找一个点开始:如0.

  把0加入到Points里,把点0能直接去的点加入到pq里。(为了方便理解,这里的边按权值递增排进数组,实际操作只需从数组中输出最小值,不必排序)

  

  然后数组输出并移除最小值:7. 把7对应的边7-0加入到mst里,把7加入Points里。

  点7能直接去的、不在Points里的点(1,2,5,4)加入到pq,但是2,4已经在pq里了,7-2的权值0.34比pq里面的0-2的权值大,故无视之;7-4的权值0.34比pq里面的0-4的权值大,故无视之。

  

  然后数组输出并移除最小值:1. 把1对应的边7-1加入到mst里,把1加入Points里。

  点1能直接去的、不在Points里的点(3,2,5)加入到pq,但是2,5已经在pq里了,1-2的权值0.36比pq里面的0-2的权值大,故无视之;1-5的权值0.32比pq里面的7-5的权值大,故无视之。

  

  然后数组输出并移除最小值:2. 把2对应的边0-2加入到mst里,把2加入Points里。

  点2能直接去的、不在Points里的点(3,6)加入到pq,但是3,6已经在pq里了,3-2的权值0.17比pq里面的1-3的权值小,故取代之;6-2的权值0.4比pq里面的0-6的权值小,故取代之。

  

  然后数组输出并移除最小值:3. 把3对应的边2-3加入到mst里,把3加入Points里。

  点3能直接去的、不在Points里的点(6)加入到pq,但是6已经在pq里了,3-6的权值0.52比pq里面的6-2的权值大,故无视之。

  

  然后数组输出并移除最小值:5. 把5对应的边7-5加入到mst里,把5加入Points里。

  点5能直接去的、不在Points里的点(4)加入到pq,但是4已经在pq里了,5-4的权值0.35比pq里面的0-4的权值小,故取代之。

  

  然后数组输出并移除最小值:4. 把4对应的边5-4加入到mst里,把4加入Points里。

  点4能直接去的、不在Points里的点(6)加入到pq,但是6已经在pq里了,4-6的权值0.93比pq里面的6-2的权值大,故无视之。

  

  然后数组输出并移除最小值:6. 把6对应的边6-2加入到mst里,把6加入Points里。

  点6没有能直接去的、不在Points里的点。最小生成树生成完毕,结束算法。

  

  

  总结一下通用思路:

  1.  首先随便选一个点加进Points。

  2.  然后把这个点能直接去的、不在Points里的点加入到数组pq中,把对应的那些边记录下来。如果要加的点已经存在于pq中,则看新加入的对应的边权值是否比已存在的小,如果是则取代之;否则无视之。

  3.  数组输出并移除拥有最小值的点,把这个点加进Points,这个点对应的边加进mst里。

  4.  重复2-3步,直到pq没有元素为止。

  这个算法贪心在哪?

  与懒惰版对比一下就很显然了: 第二步,懒惰版是直接把所有边塞进数组里,而贪心版是全部逐一比较(a会马上打扫房间,甚至把走廊也扫了一遍)。