5.6.2 普里姆(Prim)算法

1. 算法思想

普里姆算法从另一个角度来构造无向连通网的最小生成树。其基本思想是:假设无向网表示为 ​ G=(V,E) ​ V 为网中所有顶点的集合,​ E 为网中所有带权的边集合。最小生成树中的顶点集合用 ​ U 表示,最小生成树中顶点之间的边集用 ​ T 表示,​ U ​ T 初始均为空集。首先在网中任选一个顶点 ​ u 作为最小生成树的根节点,无向网中的顶点分为两类,一类是属于生成树的顶点集,表示为 ​ U = \{u\} ,另一类是不属于生成树的顶点集为 ​ (V - U) 。然后在 ​ (V - U) 中选择顶点 ​ v 加入最小生成树顶点集 ​ U 中,​ v 应满足:​ v ​ U 中的顶点通过边相邻接,且该边上的权值是在连接这两类顶点的所有边中权值最小的,该边即是最小生成树两个节点之间的分支,把它加入最小生成树的边集 ​ T 中。重复上述过程,直到 ​U = V ,即最小生成树中包含了无向网中的所有顶点,由 ​U 中的顶点和 ​ T 中的边就构成了无向网的最小生成树。

2. 算法举例

以图 5.18 (a) 所示的无向网为例(与图 5.17 (a) 相同),采用普里姆算法构造它的最小生成树,过程如图 5.18 (b) 至图 5.18 (f) 所示。假设首先选择顶点 ​ v_2 作为最小生成树的根,此时只有顶点 ​ v_2 在生成树中(如图 5.18 (b))。其余顶点 ​ v_1 ​ v_3 ​ v_4 ​ v_5 均不在生成树上,连接这两类顶点的边有 ​ (v_2, v_1) ​ (v_2, v_3) ​ (v_2, v_5) ​ (v_2, v_4) ,其中边 ​ (v_2, v_1) 权值最小,选择该边将顶点 ​ v_1 加入生成树(如图 5.18 (c))。之后连接 ​ \{v_1, v_2\} ​ \{v_3, v_4, v_5\} 两个顶点集的边,除了 ​ (v_2, v_3) ​ (v_2, v_5) ​ (v_2, v_4) ,又增加了 ​ (v_1, v_3) ​ (v_1, v_5) ​ (v_1, v_4) ,这六条边中权值最小的边为 ​ (v_1, v_4) ,选该边将顶点 ​ v_4 加入生成树(如图 5.18 (d))。之后在连接顶点集 ​ \{v_1, v_2, v_4\} ​ \{v_3, v_5\} 的边集 ​ \{(v_1, v_3), (v_1, v_5), (v_2, v_3), (v_2, v_5), (v_4, v_3), (v_4, v_5)\} 中选择权值最小的边 ​ (v_4, v_5) ,依次类推,直到所有顶点加入最小生成树(如图 5.18 (e) 和图 5.18 (f))。

5.18.png

#include<bits/stdc++.h>
using namespace std;
int g[101][101];//链接矩阵 左边横坐标表示起点,上边纵坐标表示终点。
//对应数组元素的值表示起点到终点的费用
int lowcost[101];//lowcost[j]表示的是节点到U集合的最短距离
// lowcost[j] == 0表示在U集合内, lowcost[j] != 0表示在V-U集合内
int closest[101];// (j,closest[j])是顶点j的最小边,权值为lowcost[j]
const int INF=999999;//无穷大
int n;
int sum;//保存最小生成树的权值之和
int prim(int v)
{
    int minv;
    int i,j,k;
    sum=0;
    for(i=1;i<=n;i++)//初始化
    {
        lowcost[i]=g[v][i];//初始时lowcost[i]是起点v到i的边上的权值
        closest[i]=v;//因为初始时U里就是一个v所以v是其余V-U集合中所有节点的最近关系者
    }
    for(i=1;i<n;i++)//循环n-1次,把除了起点v的其余n-1个节点 加入U集合中,就大功告成
    {
        minv=INF;
        for(j=1;j<=n;j++)//遍历不在U集合中的节点(也就是V-U集合中的节点)
        {
            if(lowcost[j]!=0 && lowcost[j]<minv)//通过 lowcost[j]!=0控制在哪个集合
            {
                k=j;//女朋友算法 k保存的是本轮遍历到目前的j到U集合的最短距离
                minv=lowcost[j];//
            }
        }


        sum+=minv;//本轮加入的边的权值就是min 也就是 lowcost[k]
        lowcost[k]=0;//选出的k加入U集合了 就代表 距离为0了,所以lowcost[k]变为0


        for(j=1;j<=n;j++)//选出来的k加入了U集合,就要交投名状
        {//遍历 lowcost[j] 看原来每个V-U中的节点到U集合的距离,比k到 每个V-U中的节点的距离远
            if(lowcost[j]!=0 && g[k][j]<lowcost[j])//那么就更新成k到 每个V-U中的节点的距离
            {
                lowcost[j]=g[k][j];//距离更新
                closest[j]=k;//最近的关系人更新
            }
        }
    }
    return 0;
}

int main()
{
    int i,j;
    cin>>n;
    for(i=1;i<=n;i++)//直接输入方案,也就是直接给二维数组
    {
        for(j=1;j<=n;j++)
        {
            cin>>g[i][j];
        }
    }
    prim(1);
    cout<<sum;
    return 0;
}

为实现普里姆算法需设置两个辅助数组 ​ lowcost ​ closest ,保存当前时刻从集合 ​ U 到集合 ​ V - U 中每个顶点的权值最小边。​ lowcost[j] 表示节点 ​ j ​ U 集合的最短距离,​ lowcost[j] == 0 表示在 ​ U 集合内,​ lowcost[j] != 0 表示在 ​ V - U 集合内。​ closest 保存该权值最小边在 ​ U 上的顶点,​ (j, closest[j]) 是顶点 ​ j 的最小边,权值为 ​ lowcost[j]

算法首先对 ​ lowcost 数组和 ​ closest 数组初始化。接着循环 ​ n - 1 次,将除起点外的其余 ​ n - 1 个顶点加入 ​ U 集合。遍历不在 ​ U 集合的节点,找出当前阶段距离 ​ U 集合最近的顶点 ​ k ,加入 ​ U 集合,同时将 ​ lowcost[k] (即 ​ minv )累加到 ​ sumv 。然后再次遍历不在 ​ U 集合的顶点,若上一阶段 ​ V - U 的顶点到 ​ U 集的最短距离大于当前阶段 ​ k 加入 ​ U 后这些点到 ​ k 的距离,则更新该顶点到 ​ U 集的最短距离为到 ​ k 的距离。

表 5.1 给出了用上述算法构造网图 5.18 (a) 的最小生成树过程中,​ closest ​ lowcost 以及集合 ​ U ​ T 的变化情况,可加深对普里姆算法的理解。

b5.1.png

普里姆算法的时间复杂度为 ​ O(n^2) ​ n 为顶点数,适用于稠密图。其思想是局部最优(贪心算法思想)加以调整达到全局最优。