Prim算法(也称为普里姆算法)可用于在带权连通图中搜索最小生成树,是解决最小生成树问题,非常经典的算法。
问题描述:
G = (V,E)是一个具有n个顶点的带权连通无向图
T = (U,TE)是图G的最小生成树,其中U是T的顶点集合,TE是T的边集合
图1-无向带权连通图
1. 构造最小生成树的步骤
由图G构造最小生成树T的步骤:
图2-构造最小生成树T的步骤
1 . 在开始的时候,如图2所示,我们根据图G中的顶点集合V中随机选取一个顶点0, 作为生成树中的顶点集合U中的初始点,顶点0到集合V - U中的所有顶点的边为候选边
。
2 . 重复以下步骤n-1次,在U中加入其他n-1个顶点。
(1)从候选边中挑选权值最小的边加入TE
,设该边在集合V-U中的顶点是k,将k加入U中;
(2)考察当前集合V-U中的所有顶点i,修改候选边:若(k,i)的权值小于原来和顶点k关联的候选边,则用(k,i)取代后者作为候选边,因为我们在选取候选边时,总是选择权值最小的边
。
2. Prim算法示例演示(起始点0)
图3-Prim算法示例演示1
这个带权连通无向图中,选取顶点0作为生成树的集合U中的起始点,而顶点0作为起始点,(0,1)有一条边,(0,2)有一条边,(0,3)也有一条边,因此我们可以从这三条边中选择权值最小的边,即(0,2)这条边加入到生成树中。
图4-Prim算法示例演示2
当把顶点2加入到集合U后,我们发现可以选择的候选边更多了。比如顶点2有(2,1),(2,3),(2,4),(2,5)四条边。通过对比这几条边的权值发现,只有(2,5)这条边的权值最小,即把(2,5)这条边加入到生成树中。
图5-Prim算法示例演示3
当把顶点5加入到集合U后,顶点5有(5,4)和(5,3)两条候选边,再次通过对比这几条边的权值,发现只有(5,3)这条边的权值是最小的,于是就可以把(5,3)这条边加入到生成树中。
图6-Prim算法示例演示4
当把顶点3加入到集合U后,根据构造生成树的几条准则,(3,0)和(3,2)这两条边会出现回路,所以不能加入到生成树中,那么只有(2,1)这条边的权值最小,因此可以把(2,1)这条边加入到生成树当中。
图7-Prim算法示例演示5
当把顶点4加入到集合U中后,即(1,4)这条权值最小的边,最终就构成了一棵生成树。
通过这个过程我们知道Prim算法在构造生成树时,每次都是选取权值最小的边,保证每一次的选择都是最优的,这其实跟贪心算法的思想是很类似的,另外,我们在构造一棵生成树的时候还需要注意几条构造生成树的准则。
3. Prim算法构造最小生成树的过程
对于Prim算法的原理我们已经介绍完了,但是对于Prim算法如何把权值最小的边加入进来,组成最小生成树的,相信有很多同学也想知道这其中到底是怎么一回事,其实要理解这一点,我们需要来了解Prim算法中两个比较关键的数组:lowcost数组和closest数组。
int lowcost[MAXV]; //记录从U到U-V的边的最小权值
int closest[MAXV]; //记录最小权值的边对应的顶点
图8-closest和lowcost数组
lowcost数组主要是记录集合U中的顶点到V-U集合中所有顶点的边的最小权值。而lowcost数组中的下标0 -5就代表顶点0 - 5,而lowcost[0]数组元素的值就代表边的权值。
图G作为一个带权无向图,是采用邻接矩阵存储的,那么邻接矩阵的第n行就是顶点n到达其他顶点的边的权值。换句话说,邻接矩阵的第0行中就是顶点0到达其他顶点的边的权值。
比如集合U中的顶点0到集合V-U中的顶点1的权值为6,那么lowcost[1]的值就为6,而顶点0无法直接到达顶点4或顶点5,因此lowcost[4]和lowcost[5]的值用符号”∞”表示(即无穷大)。
closest数组主要是记录权值最小的边对应的顶点(可以认为是记录集合U中的顶点)。因为在开始时,集合U中只有一个顶点0。
图9-构造最小生成树1
下面我们来看一下这个顶点2是如何加入到集合U中的,有同学会说当然是从V-U集合中搜索最小权值的边了,而lowcost数组中则恰好是存储了这样的顶点的,因此就需要从lowcost数组中搜索,找权值不为0,且小于无穷大的邻接点了,同时这个邻接点的权值还必须是最小的
,于是我们就能从lowcost数组中搜索顶点0的所有邻接点,找到了权值最小的邻接点,即顶点2。
当我们确定了要把顶点2加入到集合U时,同时还必须修改lowcost数组中顶点2的权值为0,表示顶点0到顶点2是没有距离的,已经加入到集合U中。
当顶点2已经加入到集合U后,还要修改顶点2到其他顶点的边的最小权值(lowcost数组主要是记录集合U中的顶点到V-U集合中所有顶点的边的最小权值),比如:(2,1),(2,3),(2,4),(2,5)这些边的权值,因为(2,1)这条边的权值为5,所以lowcost[1]的值修改为5,因为(2,3)这条边的权值本来就是5,所以lowcost[3]的值不需要修改,(2,4)对应的lowcost[4]的值修改为6,(2,5)对应的lowcost[5]的值修改为4。
同时还要修改closest数组中closest[1],closest[4],closest[5]的值修改为2,此时closest数组中记录权值最小的边对应的顶点,也就是说closest[1] = 2就代表(2,1)这条边最小权值的顶点2,其他以此类推……
图10-构造最小生成树2
然后我们根据顶点2再从lowcost数组中搜索权值最小的边的顶点,发现lowcost[5] = 4的权值是最小的,因此把顶点5加入到集合U中。同时修改lowcost[5] = 0,表示顶点2到顶点5没有距离(权值为0),顶点5已经加入到集合U中了。
然后再调整顶点5到其他顶点的边的权值,即(5,3)和(5,4)这两条边,因为(5,3)这条边的权值是2 ,所以在lowcost[3] = 5修改为2,因为(5,4)这条边的权值本来就是6,所以lowcost[4]的值不用修改。
同时还要修改closest数组中closest[3]的值修改为5,此时closest数组中记录权值最小的边对应的顶点,也就是说closest[3] = 5就代表(5,3)这条边最小权值的顶点5
图11-构造最小生成树3
然后根据顶点5从lowcost数组中搜索权值最小的边的顶点,发现lowcost[3] = 2的权值是最小的,因此把顶点3加入到集合U中,同时把lowcost[3]的值修改为0,此时3没有邻接点(因为顶点0和顶点2会出现回路,所以不能成为顶点3的邻接点),不需要再调整lowcost数组和closest数组了。
图12-构造生成树4
从lowcost数组中搜索权值最小的边的顶点,发现lowcost[1] = 5的权值是最小的,把顶点1加入到集合U中,同时把lowcost[1]的值修改为0,再调整顶点1到其他顶点的边的权值,即(1,4)这条边,(1,4)因为的权值是3,所以将lowcost[4]的值修改为3。
同时还要修改closest数组中closest[4]的值修改为1,此时closest数组中记录权值最小的边对应的顶点,即顶点1 。
图13-构造生成树5
最后从lowcost数组中再次搜索权值最小的边,把顶点4加入到集合U中,同时把lowcost[4]的值修改为0。此时lowcost数组的值全部都为0,说明全部的顶点已经都加入到集合U中,形成了最终最小的生成树
。
4. Prim算法实现
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXV 6
#define INF 99
//图的定义:邻接矩阵
typedef struct MGRAPH{
int n; //顶点数
int e; //边数
int edges[MAXV][MAXV]; //邻接矩阵
} MGraph;
//Prim算法
void Prim(MGraph g,int v)
{
//变量声明
int lowcost[MAXV];
int min;
int closest[MAXV],i,j,k;
//对lowcost和closest数组进行初始化
for (i=0; i<g.n; i++)
{
//把邻接矩阵中的每一行赋值到lowcost数组中
lowcost[i]=g.edges[v][i];
//把closest数组初始为0(因为我们是以顶点0为起始点的)
closest[i]=v;
}
//除了顶点0外,需要搜索其他所有顶点
for (i=1; i<g.n; i++)
{
//找权值最小的邻接点k
//min一开始赋值为无穷大
min=INF;
for (j=0; j<g.n; j++)
//找权值不为0,且小于无穷大的邻接点了
if (lowcost[j]!=0 && lowcost[j]<min)
{
min=lowcost[j]; //这一步操作是为了找最小权值
k=j; //k会记录权值最小的邻接点
}
//然后输出邻接点信息
printf(" \t边(%d,%d),权值:%d\n",closest[k],k,min);
//调整lowcost和closest
lowcost[k]=0; //把找到的邻接点置为0
for (j=0; j<g.n; j++)
//在调整的时候,权值不能为0,且权值要小于lowcost数组中原有的权值
if (g.edges[k][j]!=0 && g.edges[k][j]<lowcost[j])
{
lowcost[j]=g.edges[k][j];
closest[j]=k;
}
}
}
int main(void)
{
//用99数值代表无穷大
int A[MAXV][MAXV] = {
{0,6,1,5,99,99},
{6,0,5,99,3,99},
{1,5,0,5,6,4},
{5,99,5,0,99,2},
{99,3,6,99,0,6},
{99,99,4,2,6,0}
};
int i;
int j;
//以顶点0为起始点
int v = 0;
//定义邻接矩阵存储结构
MGraph g;
//边数
g.e = 10;
//顶点数
g.n = 6;
for(i = 0; i < g.n; i++)
{
for(j = 0; j < g.n; j++)
{
g.edges[i][j] = A[i][j];
}
}
printf("\n");
printf("最小生成树:\n");
//Prim算法
Prim(g,v);
printf("\n");
return 0;
}
测试结果: