深入浅出Prim算法

最小生成树的构建(Prim算法)

最近,在学习数据结构和算法,接触一些图像分割算法的时候,发现实验室的图像分割方法中涉及了最小生成树的算法,感觉比较有趣,所以看看相关的资料,最后作一点整理,希望能浅显地说明白这个算法。

首先,构建最小生成树的方法有多种,我这里介绍的是Prim算法。
其算法使用的数据结构是:使用图的顶点和边权值的邻接矩阵。
算法的主要流程:

设连通的无向图(如下图所示)





(1)列出初始点(程序中将V0看作是初始点,实际上可以任意一个点)到各顶点的距离(到自己的距离为0,不可达的为65536),记录在lowcost数组中,将adjvex初始化为大小和lowcost数组一样的零数组,表明到这个顶点的上一个顶点是初始点V0。

从这个初始化步骤,我们应该可以看出,lowcost记录的是到点的距离,然后,adjvex记录的是对应的上一顶点的下标;

(2)找出lowcost中的非0最小值,打印出它对应的adjvex值和其下标值,实际上,就是打印上一顶点和当前顶点,并将当前顶点对应的lowcost值置0,表明当前顶点已经加入到最小生成树中了;

(3)比较(2)中的当前顶点到各顶点的距离和lowcost数组中记录的距离值,如果比它记录的值小,则替代它的值,并修改对应adjvex的值为当前顶点的下标;

(4)重复(2)步骤,直到遍历了所有顶点为止。


实际上,整个Prim算法,就是对lowcost和adjvex进行更新。针对上图所示的生成树,它的部分更新过程为:


其实,搞懂了这两个数组的运行流程,再去看下面的编程实现,就不难了!

Prim算法的C++实现

#include <stdio.h>
#define ElemType char // 图中数据的类型
#define MAXVEX 100 // 最大顶点数
#define INFINTY 65535 // 用于初始化邻接矩阵
#define NUMVEX 9
#define NUMEDGE 15

// 邻接矩阵的结构
struct MGraph 
{
    ElemType vertex[MAXVEX]; // 顶点数组
    int arc[MAXVEX][MAXVEX]; // 边二维数组,表示两点之间有无连接
    int numVex, numEdges; // 顶点和边的数量
};

// 构建图
// 实际上就是给MGraph的结构体赋值
int CreateGraph(MGraph* g)
{
    int i,j,k;
    char vertexes[10] = {'A','B','C','D','E','F','G','H','I'}; 
    int vertexes1[15] = {0,0,1,1,1,2,2,8,6,6,6,3,3,7,5}; // 顶点的下标
    int vertexes2[15] = {1,5,2,6,8,8,3,3,3,7,5,7,4,4,4};
    int weights[15] = {10,11,18,16,12,8,22,21,24,19,17,16,20,7,26};
    char ch;

    g->numVex = 9; 
    g->numEdges = 15;

    // 创建顶点数组
    for (i=0;i<g->numVex;++i)
    {
        g->vertex[i] = vertexes[i];
    }

    // 创建边数组
    // 首先进行初始化
    for (j=0;j<g->numVex;++j)
    {
        for (k=0;k<g->numVex;++k)
        {
            g->arc[j][k] = INFINTY;
        }
    }

    // 然后输入两点之间的连接情况
    for (k=0;k<g->numEdges;++k)
    {
        i = vertexes1[k];
        j = vertexes2[k];
        g->arc[i][j] = weights[k];
        g->arc[j][i] = g->arc[i][j];
    }
    return 1;
}

/* Prim算法生成最小生成树 */
void MiniSpanTree_Prim(MGraph G)
{
    int min,i,j,k;
    int adjvex[MAXVEX];  // 保存代价最小对应的顶点的下标
    int lowcost[MAXVEX]; // 保存顶点间的边的代价/权值

    // 初始化
    lowcost[0] = 0; // 初始化第一个点的权值为0,表明A已经加入生成树中了
    adjvex[0] = 0; // 初始化第一个顶点下标为0
    for (i=1;i<G.numVex;++i)
    {
        lowcost[i] = G.arc[0][i]; // 将第一个顶点(A)顶点与之有边的权值存入数组
        adjvex[i] = 0;  // 都初始化为A的下标(0)
    }
    // 开始构造最小生成树
    for (i=1;i<G.numVex;++i)
    {
        min = INFINTY;
        j=1;k=0;
        while (j<G.numVex)
        {
            if (lowcost[j]!=0 && lowcost[j]<min) // lowcost[j]==0为真,则表明该点已经是最小生成树的了
            {
                min = lowcost[j]; // 让当前权值成为最小值
                k = j; // 记住当前最小值的下标
            }
            ++j;
        }
        printf("(%d,%d)\n",adjvex[k],k); // k是当前最小值对应的下标,adjvex[k]是上一个顶点
        lowcost[k] = 0; // 将当前的顶点的权值设置为0,表示此顶点已经完成任务
        // 循环所有点,更新lowcost 和 adjvex
        for (j=1;j<G.numVex;j++)
        {
            if (lowcost[j]!=0 && G.arc[k][j]<lowcost[j])
            {
                lowcost[j] = G.arc[k][j];
                adjvex[j] = k;
            }
        }

        // 仅用于显示所用
        printf("lowcost: ");
        printmat(lowcost,9);
        printf("\n");
        printf("adjvex: ");
        printmat(adjvex,9);
        printf("\n\n");
    }
}

int main()
{
    MGraph G;
    CreateGraph(&G); // 创建图
    MiniSpanTree_Prim(G);

    return 0;
}

运行结果:


结果和我之前手写的结果一致。

Prim算法的应用

实际应用,就举一个acm的题目为例。

http://acm.hdu.edu.cn/showproblem.php?pid=1863

问题:
省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可)。经过调查评估,得到的统计表中列出了有可能建设公路的若干条道路的成本。现请你编写程序,计算出全省畅通需要的最低成本。

Input:
测试输入包含若干测试用例。每个测试用例的第1行给出评估的道路条数 N、村庄数目M ( < 100 );随后的 N 
行对应村庄间道路的成本,每行给出一对正整数,分别是两个村庄的编号,以及此两村庄间道路的成本(也是正整数)。为简单起见,村庄从1到M编号。当N0时,全部输入结束,相应的结果不要输出。

Sample Input:
3 3
1 2 1
1 3 2
2 3 4
1 3
2 3 2
0 100

Sample Output:
3
?

理解题意以后,我发现之前写的Prim算法,需要添加一点异常处理信息,就是能判断构建图是否能连通,后面的代码实现会有所实现。

实现思路

添加判断图是否能连通的异常处理,使用全局变量替代结构体。

实现的代码:

#include <stdio.h>
#define MAXVEX 101 // 村庄的最大数,即最大顶点数
#define INFINTY 65536 // 最大成本数

// Prim算法的实际应用,输入
int vertex[MAXVEX];
int arc[MAXVEX][MAXVEX]; // 代价值
int numEdges; // 边数,即道路数
int numVex; // 点数,即村庄数

// 初始化arc数组 
void init()
{
    int i,j;
    for (i=0;i<MAXVEX;++i)
    {
        for (j=0;j<MAXVEX;++j)
        {
            arc[i][j] = INFINTY;
        }
    }
}

/* Prim算法生成最小生成树 */
// 输入是顶点数n,返回的是最小成本
int MiniSpanTree_Prim(int n)
{
    int min,i,j,k;
    int sumcost = 0;
    int adjvex[MAXVEX];  // 保存代价最小对应的顶点的下标
    int lowcost[MAXVEX]; // 保存顶点间的边的代价/权值

    // 初始化
    lowcost[0] = 0; // 初始化第一个点的权值为0,表明A已经加入生成树中了
    adjvex[0] = 0; // 初始化第一个顶点下标为0
    for (i=1;i<n;++i)
    {
        lowcost[i] = arc[0][i]; // 将第一个顶点(A)顶点与之有边的权值存入数组
        adjvex[i] = 0;  // 都初始化为A的下标(0)
    }

    // 开始构造最小生成树
    for (i=1;i<n;++i)
    {
        min = INFINTY;
        j=1;k=0;
        while (j<n)
        {
            if (lowcost[j]!=0 && lowcost[j]<min) // lowcost[j]==0为真,则表明该点已经是最小生成树的了
            {
                min = lowcost[j]; // 让当前权值成为最小值
                k = j; // 记住当前最小值的下标
            }
            ++j;
        }
        // 判断图是否能连通
        if(min==INFINTY)
             return -1;// 当更新后,min都还是INFINTY,表明它不能连通其他顶点!
        sumcost += min;
        //printf("(%d,%d)\n",adjvex[k],k); // k是当前最小值对应的下标,adjvex[k]是上一个顶点
        lowcost[k] = 0; // 将当前的顶点的权值设置为0,表示此顶点已经完成任务
        // 循环所有点,更新lowcost 和 adjvex
        for (j=1;j<n;j++)
        {
            if (lowcost[j]!=0 && arc[k][j]<lowcost[j])
            {
                lowcost[j] = arc[k][j];
                adjvex[j] = k;
            }
        }
    }
    return sumcost;
}

int main()
{   
    int n,m;
    int a,b,w;
    int ans;
    while(1)
    {
        init(); // 将arc数组初始化
        // printf("input the number of roads and villages: ");
        fflush(stdin);
        scanf("%d %d",&n,&m);
        if(n==0)
            return 0;
        for (int i=0;i<n;++i) // 成本的个数和道路是一致的
        {
            fflush(stdin);
            scanf("%d %d %d",&a,&b,&w);
            if (arc[a-1][b-1]>w)
            {
                arc[a-1][b-1] = w;
            }
        }
        ans = MiniSpanTree_Prim(m);
        if(ans==-1)
        {
            putchar('?');
        }
        else
            printf("%d",ans);
    }
    return 0;
}

运行结果:




运行结果正确

猜你喜欢

转载自blog.csdn.net/louishao/article/details/78721868