• 图表算法—最小生成树


    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会马上打扫房间,甚至把走廊也扫了一遍)。

  • 相关阅读:
    如何快速定位到DBGrid的某一行!!!急...
    说说设计模式~单件模式(Singleton)
    EF架构~终于自己架构了一个相对完整的EF方案
    .Net——实现IConfigurationSectionHandler接口定义处理程序处理自定义节点
    在LINQ中实现多条件联合主键LEFT JOIN
    从LINQ开始之LINQ to Objects(上)
    WPF控件模板和数据模板
    WPF的ListView控件自定义布局用法实例
    初步探讨WPF的ListView控件(涉及模板、查找子控件)
    LINQ技巧:如何通过多次调用GroupBy实现分组嵌套
  • 原文地址:https://www.cnblogs.com/mcomco/p/10334536.html
Copyright © 2020-2023  润新知