图论(四):最小生成树算法和Prim实现

H=(V,T)是图G=(V,E)的子图,H是图G的生成树,当且仅当H是一个无环的连通图。生成树具有以下性质:

  • H有|V-1|条边与|V|个顶点
  • H是最小化连接的:去除任意一条边都会是H不连通
  • H是最大无环结构:添加任意一条边都会构造一个环

给出一个无向加权图G=(V,E),最小生成树问题(minimun spanning tree,MST):求一个生成树使得每条边的权值的和最小。

本节在讨论解决最小生成树的两种贪心算法:Kruskal算法与Prim算法之前,先说明通用的、形式化的最小生成树的生成算法,这个是证明两个算法正确性的关键。然后说明这两种贪心算法,最后给出Prim算法的C++实现。


一 最小生成树的形成

假设A是某棵最小生成树的子集,生成最小生成树的算法就是循环选择一条合适的边(u,v),将其加入到A中,同时满足A是某棵最小生成树的子集的条件,我们把这样的边(u,v)称为集合A的安全边。因此通用最小生成树的伪代码:

这里的关键是辨认安全边的规则。介绍一些定义方便证明算法的正确性:

  • 切割(cut):无向图G=(V,E)的一个切割(S,V-S)是集合V的一个划分

  • 横跨(cross):如果一条边 ( u , v ) ∈ E (u,v)\in E (u,v)E的一个端点在集合S中,另一个端点在集合V-S中,则称该边横跨cut(S,V-S)

  • 尊重(respect):集合A中不包含cross(u,v),则称该切割尊重集合A

  • 轻边(light edge):在横跨一个切割的所有边中,权重最小的边称为轻边

**定理1:辨析安全边的规则:设(u,v)是横跨切割(S,V-S)的轻量边,且切割(S,V-S)尊重最小生成树的子集A,那么边(u,v)对于集合A来说就是安全的。**下面是证明过程:

假设T是一棵包括A的最小生成树,边(u,v)分为以下两种情况:

  • case1:如果T包含了轻边(u,v),(u,v)对于A来说一定是安全的。

  • case2:T不包含轻边(u,v),为了说明,(u,v)对于A来说一定是安全的,我们构造一个最小生成树 T ‘ T^` T,使得它包含 A ⋃ ( u , v ) A\bigcup{(u,v)} A(u,v)

    在这里插入图片描述

    因为T不包含(u,v),边(u,v)与T中从节点u到节点v的唯一简单路径p形成一个环,如上图所示。由于u与v属于切割(黑节点,白节点)的两端,T中至少有一条边(x,y)属于路径p且横跨该切割。

    又因为切割尊重集合A,所以边(x,y) 不在集合A中,我们将(u,v)替换(x,y)得到一棵新的生成树 T ‘ = T − { ( x , y ) } ⋃ { ( u , v ) } T^`=T-\{(x,y)\}\bigcup \{(u,v)\} T=T{(x,y)}{(u,v)}

    下面证明 T ‘ T^` T也是一棵最小生成树:由于(x,y)与轻边(u,v)都横跨切割(S,V-S),有 w ( u , v ) < w ( x , y ) w(u,v)<w(x,y) w(u,v)<w(x,y),因此:
    w ( T ‘ ) = w ( T ) − w ( x , y ) + w ( u , v ) ≤ w ( T ) w(T^`)=w(T)-w(x,y)+w(u,v)\leq w(T) w(T)=w(T)w(x,y)+w(u,v)w(T)
    因为T是一棵MST,所以 T ‘ T^` T同样也是一棵MST。这样的话(u,v)对于A来说就是安全边。

后面要介绍的两个算法就是使用了定理2:辨析安全边的推论

设图G=(V,E)是一个连通无向图,定义了一个权重函数w。设集合A为G的MST的顶点子集,设森林 G A = ( V , A ) G_A=(V,A) GA=(V,A)中,如果边(u,v)是连接 G A G_A GA的两个连通分量的轻边,则(u,v)对于A来说是安全的。

Kruskal算法和Prim算法都属于贪心算法,都使用了一条具体的贪心规则来确定GENERIC-MST算法第三行所描述的安全边:

  • 在Kruskal算法中,集合A(某棵最小生成树的子集)是一个森林,图中的结点就是森林的结点,每次加到集合A中的安全边永远是权重最小的连接两个不同分量的边
  • 在Prim算法中,集合A是一棵树,每次加入到A中的安全边永远是连接A与A之外某个节点的边中权重最小的边

二 Kruskal算法

Kruskal算法找到安全边的办法是,在所有连接森林中两棵不同树的边里面(在森林中,一个结点也可以看作一棵树),找到权重最小的边(u,v)。根据辨析安全边的推论【定理2】,这条边(u,v)是对A安全边(u-v是连接了 G A G_A GA中的两个连通分量的轻边),下图为示例:

Kruskal算法实现依赖于并查集这一数据结构(后面会补充),每个集合代表当前森林中的一棵树,操作FIND-SET(u)返回包含元素u的集合的代表元素,可以通过测试FIND-SET(u)是否等于FIND-SET(v)来判断结点u与节点v是否属于同一棵树,通过操作Union过程来对两棵树进行合并。伪代码如下:

Kruskal实现的方式准备放在并查集的那节中。

三 Prim算法

3.1 原理与伪代码

Prim算法特性是,集合A中的边总是构成一棵树,这棵树从任意的根节点r开始,一直长大到覆盖V中所有的结点为止,算法每一步在连接集合A和A之外的结点的所有边中,选择一条轻边加入A中。根据辨析安全边的规则【定理1】,这条边(u,v)是对A安全边:以(A,V-A)作为图的切割,选择一条横跨切割的轻边。

为了有效的实现Prim算法,需要一种快速的方法来选择一条新的边加入到集合A所构成的树中,伪代码中无向连通图G与最小生成树的根节点为r,所有不在树A中的结点保存在基于key属性的最小优先队列Q中。对于每个节点v,属性v.key保存的是连接v与树A中的节点的所有直连边中最小的权值,如果没有直连边,则 v . k e y = ∞ v.key=\infty v.key=。属性v.π给出的是结点v在树中的父节点。当Prim算法终止时,最小优先队列Q将为空,而G中的MST-A的状态为:
A = { ( v , v . π ) : v ∈ V − { r } } A=\{(v,v.π): v\in V-\{r\}\} A={(v,v.π):vV{r}}

时间复杂度分析

3.2 C++实现

/**
 * 最小生成树的Prim算法
 */
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;

int key[100];//结点n到最小生成树的直连最小权值

//自定义priority_queue的优先顺序
struct cmp{
    bool operator ()(int &a,int &b){
        return key[a]>key[b];//key最小值优先
    }
};

/**
 * Prim算法
 * @param n 结点个数,结点标号从0~n-1
 * @param edges 边集合 一个向量表示{node1,node2,weight}
 * @param start 起始节点
 */
void Prim(int n,vector<vector<int>> &edges,int start){
    vector<int> A;// MST的顶点子集
    int p[n];// 前驱结点
    for(int i=0;i<n;i++){
        key[i] = INT_MAX;
        p[i] = -1;// 没有前驱节点
    }
    key[start] = 0;// 设置起始节点

    priority_queue<int,vector<int>,cmp> pq;// key最小优先队列
    for(int i=0;i<n;i++)// 优先队列初始化
        pq.push(i);

    while(!pq.empty()){
        int u = pq.top();// 找到到树A权值最小的结点
        pq.pop();
        A.push_back(u);
        for(vector<int> edge:edges){
            int v = -1;// 所有边中找到与u相连的边 v指向与u相连的另一个顶点
            if(edge[0] == u)
                v = edge[1];
            if(edge[1] == u)
                v = edge[0];
            // v存在,更新不在树A的结点v的key值与p值
            if(v != -1 && !count(A.begin(),A.end(),v)){
                if(edge[2]<key[v]){
                    key[v] = edge[2];
                    p[v] = u;
                    // 更新pq
                    priority_queue<int,vector<int>,cmp> pq1;
                    while(!pq.empty()){
                        pq1.push(pq.top());
                        pq.pop();
                    }
                    pq = pq1;
                }
            }
        }
    }
    // 打印结果
    for(int i=0;i<n;i++){
        if(i == start)
            cout<<"MST root is node "<<i<<endl;
        else
            cout<<"node "<<i<<"`s parent node is "<<p[i]<<endl;
    }
}

int main(){
    // {{0,1,1},{1,2,1},{2,3,1},{0,3,1}}
    // {{0,1,1},{1,2,1},{2,3,2},{0,3,2},{0,4,3},{3,4,3},{1,4,6}}
    vector<vector<int>> edges = {{0,1,1},{1,2,1},{2,3,2},{0,3,2},{0,4,3},{3,4,3},{1,4,6}};
    Prim(5,edges,0);
}

测试用例:

  • 示例一

  • 示例二

参考

【1】算法导论

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sunburst7

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值