最小生成树的构建(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编号。当N为0时,全部输入结束,相应的结果不要输出。
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;
}
运行结果:
运行结果正确