Prim算法的执行非常类似于寻找图的最短通路的Dijkstra算法。Prim算法的特点是集合A中的边总是只形成单棵树。如图5所示,阴影覆盖的边属于正在生成的树,树中的结点为黑色。在算法的每一步,树中的结点确定了图的一个割,并且通过该割的轻边被加进树中。树从任意根结点r开始形成并逐渐生长直至该树跨越了V中的所有结点。在每一步,连接A中某结点到V-A中某结点的轻边被加入到树中,由推论2,该规则仅加大对A安全的边,因此当算法终止时,A中的边就成为一棵最小生成树。因为每次添加到树中的边都是使树的权尽可能小的边,因此上述策略也是贪心的。
有效实现Prim算法的关键是设法较容易地选择一条新的边添加到由A的边所形成的树中,在下面的伪代码中,算法的输入是连通图G和将生成的最小生成树的根r。在算法执行过程中,不在树中的所有结点都驻留于优先级基于key域的队列Q中。对每个结点v,key[v]是连接v到树中结点的边所具有的最小权值;按常规,若不存在这样的边则key[v]=∞。域p[v]说明树中v的“父母”。在算法执行中,GENERIC-MST的集合A隐含地满足:
A={(v,p[v])|vÎV-{r}-Q}
当算法终止时,优先队列Q为空,因此G的最小生成树A满足:
A={(v,p[v])|vÎ V-{r}}
(a) | (b) |
(c) | (d) |
(e) | (f) |
(g) | (h) |
(i) |
图5 Prim算法在图1所示的图上的执行流程
MST-PRIM(G,w,r)
1. Q←V[G]
2. for 每个uÎQ
3. do key[u]←∞
4. key[r]←0
5. p[r]←NIL
6. while Q≠Æ
7. do u←EXTRACT-MIN(Q)
8. for 每个vÎAdj[u]
9. do if vÎQ and w(u,v)<key[v]
10. then p[v]←u
11. key[v]←w(u,v)
Prim算法的工作流程如图5所示。第1-4行初始化优先队列Q使其包含所有结点,置每个结点的key域为∞(除根r以外),r的key域被置为0。第5行初始化p[r]的值为NIL,这是由于r没有父母。在整个算法中,集合V-Q包含正在生长的树中的结点。第7行识别出与通过割(V-Q,Q)的一条轻边相关联的结点uÎQ(第一次迭代例外,根据第4行这时u=r)。从集合Q中去掉u后把它加入到树的结点集合V-Q中。第8-11行对与u邻接且不在树中的每个结点v的key域和p域进行更新,这样的更新保证key[v]=w(v,p[v])且(v,p[v])是连接v到树中某结点的一条轻边。
Prim算法的性能取决于我们如何实现优先队列Q。若用二叉堆来实现Q,我们可以使用过程BUILD-HEAP来实现第1-4行的初始化部分,其运行时间为O(V)。循环需执行|V|次,且由于每次EXTRACT-MIN操作需要O(㏒V)的时间,所以对EXTRACT-MIN的全部调用所占用的时间为O(V㏒V)。第8-11行的for循环总共要执行O(E)次,这是因为所有邻接表的长度和为2|E|。在for循环内部,第9行对队列Q的成员条件进行测试可以在常数时间内完成,这是由于我们可以为每个结点空出1位(bit)的空间来记录该结点是否在队列Q中,并在该结点被移出队列时随时对该位进行更新。第11行的赋值语句隐含一个对堆进行的DECREASE-KEY操作,该操作在堆上可用0(㏒V)的时间完成。因此,Prim算法的整个运行时间为O(V㏒V+E㏒V)=O(E㏒V),从渐近意义上来说,它和实现Kruskal算法的运行时间相同。
通过使用Fibonacci堆,Prim算法的渐近意义上的运行时间可得到改进。在Fibonacci堆中我们己经说明,如果|V|个元素被组织成Fibonacci堆,可以在O(㏒V)的平摊时间内完成EXTRACT-MIN操作,在O(1)的平摊时间里完成DECREASE-KEY操作(为实现第11行的代码),因此,若我们用Fibonacci堆来实现优先队列Q,Prim算法的运行时间可以改进为O(E+V㏒V)。
算法实现如下:
Write By LiveStar (2008.07.23)
*/
#include < stdio.h >
// max表示点,边的最大个数
#define max 1000
// wq表示两点的距离为无穷
#define wq 9999
// 邻接矩阵存储相应的边的权重
int mix[max][max];
// 输入并构造邻接矩阵
void input( int n, int m)
{
int i,j,sp,ep,wg;
//初始化邻接矩阵每个都是无穷大
for (i=0;i<n;i++)
for (j=0;j<n;j++)
mix[i][j]=wq;
printf("请输入边的起点、终点、权重(EX:1 2 3):/n");
for (i=0;i<m;i++)
{
scanf("%d %d %d",&sp,&ep,&wg);
//无向连通图
mix[sp][ep]=wg;
mix[ep][sp]=wg;
}
}
// 显示邻接矩阵
void output( int n, int m)
{
int i,j;
printf("/n邻接矩阵如下:/n/n");
for (i=0;i<n;i++)
{
for (j=0;j<n;j++)
printf("%d/t",mix[i][j]);
printf("/n");
}
}
// prim算法实现
void prim( int n, int r)
{
//a[max]用来存放各个点到已经标记点的集合的最短距离
int a[max];
//b[max]用来记录每个点的标记状态,false表示还没标记。
bool b[max];
int i,j,k,min;
//初始化从根节点开始
for (i=0;i<n;i++)
{
a[i]=mix[r][i];
b[i]=false;
}
b[r]=true;
printf("/n依次被标记的点及相应边的权重:/n");
printf("%d/t%d/n",r,0);
for (i=0;i<n-1;i++)//还剩n-1个点
{
k=0;
min=wq;
for (j=0;j<n;j++)
{
//第j个点到已经标记点的集合的距离最小且这个点还没有被标记
//k记录下一个将被标记的点
if (a[j]<min&&b[j]==false)
{
min=a[j];
k=j;
}
}
b[k]=true;//标记节点k
printf("%d/t%d/n",k,min);
//更新a[]里的状态,时刻保持最新的状态
//存放各个点到已经标记点的集合的最短距离
for (j=0;j<n;j++)
{
if (mix[k][j]<a[j])
{
a[j]=mix[k][j];
}
}
}
}
int main()
{
//n是点的个数 ,m是边的个数
int n,m,r;
printf("请输入点的个数和边的个数(EX:1 3):/n");
scanf("%d %d",&n,&m);
//输入并构造邻接矩阵
input(n,m);
//显示邻接矩阵
output(n,m);
while (1)
{
printf("/n请输入根节点(-1跳出):/n");
scanf("%d",&r);
if(r==-1)
break;
//prim算法实现
prim(n,r);
printf("/n/n");
}
return 0;
}
测试数据:
9 14
0 1 4
1 2 8
2 3 7
3 4 9
4 5 10
5 6 2
6 7 1
7 8 7
8 2 2
2 5 4
1 7 11
0 7 8
3 5 14
6 8 6