• 最小生成树—Prim算法


    我们要讨论的问题是如何在一个无向图中找到它的最小生成树,虽然这个问题对有向图也有意义,但是处理起来更麻烦。

    一个无向图 G 的最小生成树就是连接 G 上所有顶点的边构成的树,且这些边的总权值最低。当且仅当图是连通的才有最小生成树。

    在无向图的最小生成树中,边的条数为|V|-1。最小生成树是一棵树,因为它无圈;边的总权值最低,所以它最小;包含所有顶点,所以是生成树。显然,最小生成树是包含所有顶点的最小的树。如果我们需要给一所房子里安装电路,那么这就是一个最小生成树问题。

    对于任意生成树,如果向树中增加一条不属于树的边e,就会形成一个圈,除去圈中任意一条边,又会变回生成树。如果边e的值比除去的边值低,那么新的生成树的值就比原树小。如果在建立生成树时所添加的边在所有不成圈的边值中最小,那么最后得到的生成树就不能被改进,否则更改后的树要么成圈,要么权值更高。

    最小生成树的建立方法有两种:Prim算法和Kruskal算法。下面以下图为例,从v_{1}出发,建立该图的最小生成树。

    Prim算法:

    计算最小生成树的方法是使其连续的一步步的成长。在每一步,都要把当前节点当作根节点并往上加边。 

    Prim算法与Djkstra算法非常相似(Djkstra算法在前一篇文章),在处理当前顶点v时,先将其标记为已知,如果v的邻接顶点w未知,且对应的dvw要小于w当前的d,那么就需要更新w顶点的d以及path,与Djkstra算法不同的是,d表示的不再是从w到输入顶点s的距离,而是w与邻接顶点v的距离,所以其是否更新的判断法则也需要改变。这个算法也是一种贪婪算法,因为在每一步,我们所加入的都是目前权值最小的边,这个算法不会形成圈,因为在算法中不会再改变已知的顶点,并且每一步都将处理顶点视为根节点,所以任何顶点都只能与最后使其d改变的顶点直接相连。

    下面说明Prim算法的具体过程,从v_{1}开始建立最小生成树:

    vKnowndpath
    v_{1}00-1
    v_{2}0inf-1
    v_30inf-1
    v_40inf-1
    v_50inf-1
    v_60inf-1
    v_70inf-1

     与Djkstra算法一样,首先找到d最小的未知顶点,即v_1,将其标记为已知,并更新与它邻接的未知顶点的信息,并且当前更新边中的最小边添加进树(这个操作实际上就是标记该节点为已知,则该节点和其path节点之间的边就被添加进树中,加上边(v_1,v_1)(假设有),总共七条边添加在树中,也就是所有的顶点都被标记为已知,算法就结束):

    vKnowndpath
    v_{1}10-1
    v_{2}02v_{1}
    v_304v_{1}
    v_401v_{1}
    v_50inf-1
    v_60inf-1
    v_70inf-1

    接下来,d最小的未知顶点是v_4,将其标记为已知,并更新其邻接顶点信息,由于v_1已知,所以不考察v_1,而边(v_4,v_2)的值要大于目前v_2的d,所以v_2的信息不变,但边(v_4,v_3)的值小于v_3目前的d,所以v_3的信息要进行更新,而目前最小的边为(v_1,v_2),所以将它添加进树:

    vKnowndpath
    v_{1}10-1
    v_{2}02v_{1}
    v_302v_4
    v_411v_{1}
    v_507v_4
    v_608v_4
    v_704v_4

     下一个要选择的顶点是v_{2},并加入目前的最小边,显然它不会使任何信息改变(除过其自身的Known变为1)。

    vKnowndpath
    v_{1}10-1
    v_{2}12v_{1}
    v_302v_4
    v_411v_{1}
    v_507v_4
    v_608v_4
    v_704v_4

    下面要选择的顶点是v_3,对其进行与之前顶点同样的处理:

    vKnowndpath
    v_{1}10-1
    v_{2}12v_{1}
    v_312v_4
    v_411v_{1}
    v_507v_4
    v_605v_3
    v_704v_4

    下一步选择的顶点是v_7

    vKnowndpath
    v_{1}10-1
    v_{2}12v_{1}
    v_312v_4
    v_411v_{1}
    v_506v_7
    v_601v_7
    v_714v_4

    接下来选择v_{6},并将最小的边加入树中,它不会对任何信息产生影响 :

    vKnowndpath
    v_{1}10-1
    v_{2}12v_{1}
    v_312v_4
    v_411v_{1}
    v_506v_7
    v_611v_7
    v_714v_4

     最后考察v_{5},它也不会对任何信息产生影响:

    vKnowndpath
    v_{1}10-1
    v_{2}12v_{1}
    v_312v_4
    v_411v_{1}
    v_516v_7
    v_611v_7
    v_714v_4

    到此,就成功建立了该图的最小生成树,代码如下:

    1. void Prim(g* p) {
    2. int min, i, Dmin;
    3. for (;;) {
    4. Dmin = inf;//找到d最小的未知顶点
    5. for (i = 0; i < 7; i++) {
    6. if (p->v[i]->known == 0 && p->v[i]->d < Dmin) {
    7. min = i;
    8. Dmin = p->v[i]->d;
    9. }
    10. }
    11. if (Dmin == inf) {//满足这个条件时,要么顶点全部已知,要么图是不连通的
    12. break;
    13. }
    14. p->v[min]->known = 1;
    15. l* tmp = p->v[min]->next;
    16. while (tmp != NULL) {
    17. if (p->v[tmp->val]->known == 0) {
    18. if (p->v[tmp->val]->d > tmp->dvw) {//判断条件相比Djkstra算法改变
    19. p->v[tmp->val]->d = tmp->dvw;
    20. p->v[tmp->val]->path = min;
    21. }
    22. }
    23. tmp = tmp->next;
    24. }
    25. }
    26. }

    使用两层循环的Prim算法时间复杂度为O(|V|^2),当图稠密时,使用这样的方法是好的,如果图是稀疏的,使用二叉堆的时间复杂度为O(|E|log|V|),对于稀疏图来说,这是一个好的界(当图是稠密的,则有|E|=O(|V|^2),所以使用两层循环是合适的,当图稀疏时,不再满足这个条件,那么 O(|E|log|V|)的界就比O(|V|^2)好很多了)。前面提到当且仅当图是连通的才具有最小生成树,判断图是否连通很简单,当程序中最外层的for循环终止后,如果图中仍存在顶点的d=inf,那么就说明图是不连通的。

  • 相关阅读:
    带SLCD屏驱动的低功耗单片机MM32L0130
    Mendeley在linux中无法打开APPimage
    django超市管理系统-计算机毕业设计源码53507
    wpfui:一个开源免费具有现代化设计趋势的WPF控件库
    C语言进阶—深度剖析数据在内存中的存储
    vscode中注释多行bash脚本
    既要又要的正则匹配规则
    CentOS 升级 OpenSSL 至最新版
    Shell :抽奖小程序
    指针基础 - golang版
  • 原文地址:https://blog.csdn.net/thdwx/article/details/127457520