算法流程
如果说上个算法基于点,那这个Prim算法就是基于边的。
算法流程大致如下:
1.从任意一个人顶点开始构造生成树,假设从1号顶点开始。首先将1号顶点加入生成树中,用一个一维数组book来标记那些顶点已经加入了生成树。
2.用数组dis记录生成树到各个顶点的距离。最初生成树中只有1号顶点,有直连边时,数组dis中存储的就是1号顶点到该顶点的边的权值,没有直连边时就是无穷大,即inf。
3.从数组dis中选出离生成树最近的顶点(假设这个顶点为j)加入到生成树中(即在数组dis中找最小值)。再以j为中间点,更新生成树到每一个非树顶点的距离(就是松弛啦),即如果dis[k] > e[j][k]则更新dis[k] = e[j][k]。
4.重复第3步,直到生成树中有n个顶点为止。
代码
#include <stdio.h>
#include <iostream>
using namespace std;
#define inf 0x3f3f3f3f//用inf定义为我们认为的一个正无穷的值
int main(){
int n, m, i, j, k, min, t1, t2, t3;
int e[7][7], dis[7], book[7] = {0};//对book数组进行了初始化
int count = 0, sum = 0;//count用来记录生成树中顶点的个数,sum用来存储路径之和
//读入n和m,n表示顶点个数,m表示边的条数
cin >> n >> m;
//初始化
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
if(i == j) e[i][j] = 0;
else e[i][j] = inf;
//开始读入边
for (int i = 1; i <= m; ++i){
cin >> t1 >> t2 >>t3;
//注意这里是无向图,所以需要将边反向再存储一遍
e[t1][t2] = t3;
e[t2][t1] = t3;
}
//初始化dis数组,这里是1号顶点到各个顶点的初始距离,因为当前生成树中只有1号顶点
for (int i = 1; i <= n; ++i){
dis[i] = e[1][i];
}
//Prime算法核心部分开始
//将1号顶点加入生成树
book[1] = 1;//这里用一个book来标记一个顶点是否已经加入生成树
count ++;//count 是最小生成树中各顶点的个数,因为1号顶点已经入队所以最小生成树中顶点的个数+1
while(count < n){
min = inf;
for(i = 1; i <= n; ++i){
if(book[i] == 0 && dis[i] < min){
min = dis[i];
j = i;
}
}
book[j] = 1;
count ++;
sum += dis[j];
//扫描当前顶点j所有的边,再以j为中间点,更新生成树到每一个非树顶点的距离
for (k = 1; k <= n; ++k){
if(book[k] == 0 && dis[k] > e[j][k])
dis[k] = e[j][k];
}
}
cout << sum;
return 0;
}
测试样例:
6 9
2 4 11
3 5 13
4 6 3
5 6 4
2 3 6
4 5 7
1 2 1
3 4 9
1 3 2
运行结果:
19
优化
上面这种算法的时间复杂度是O(N2)。如果借助“堆”,每次选边的时间复杂度是O(logM),然后使用邻接表来存储图的话,整个算法的时间复杂度会降低到O(MlogN)。那么究竟如何优化呢?我们需要三个数组,如下图:
数组dis用来记录生成树到各个顶点的距离。数组h是一个最小堆,堆里面存储的是顶点编号。请注意,这里并不是按照顶点编号的大小来建立最小堆的,而是按照顶点在数组dis中对应的值来建立这个最小堆。此外还需要一个pos数组来记录每个顶点在最小堆中的位置。例如上图中,左边最小堆的圆圈中存储的是顶点编号,圆圈右下角的数是该顶点(圆圈里面的数)到生成树的最短距离,即数组dis中存储的值。
//Prim算法优化版
#include <stdio.h>
#include <iostream>
using namespace std;
#define inf 0x3f3f3f3f
int dis[7], book[7]= {0};//book数组用来记录哪些顶点已经放入生成树中
int h[7], pos[7], size;//h用来保存堆,pos用来存储每个顶点在堆中的位置,size为堆的大小。
void swap(int x, int y){
int t;
t = h[x];
h[x] = h[y];
h[y] = t;
//同步更新pos
t = pos[h[x]];
pos[h[x]] = pos[h[y]];
pos[h[y]] = t;
return ;
}
void siftdown(int i){//传入一个需要继续向下调整的节点编号
int t, flag = 0;//flag用来标记是否需要向下调整
while(i*2 <= size && flag == 0){
//比较i和它的左儿子i*2在dis中的值,并用t记录较小的点的编号
if(dis[h[i]] > dis[h[i*2]]) t = i*2;
else t = i;
//如果它有右儿子,再对右儿子进行讨论,并用t记录较小的节点编号
if(i*2+1 <= size){
//如果右儿子的值更小,更新较小的节点编号
if(dis[h[t]] > dis[h[i*2+1]]) t = i*2 + 1;
}
//如果发现最小的节点编号不是自己,说明子节点中有比父节点更小的
if(t != i){
swap(t, i);//交换他们
i = t;//更新i为刚才与它交换的儿子节点的编号,便于接下来继续向下调整
}
else flag = 1;//否则说明当前的父节点已经比两个子节点都要小了,不需要再进行调整了。
}
return ;
}
void siftup(int i){//传入一个需要向上调整的节点编号i
int flag = 0;//用来标记是否需要向上调整
if(i == 1) return ;//如果是堆顶就已经不需要向上调整了。
//不在堆顶,并且当前节点i的值比父节点小的时候就继续向上调整
while(i != 1 && flag == 0){
//判断是否比父节点的小
if(dis[h[i]] < dis[h[i/2]])
swap(i, i/2);//交换它和它爸爸的位置
else
flag = 1;//表示已经不需要调整了,当前节点的值比父节点的值要大
i = i / 2; //这句话很重要,更新编号i为它父节点的编号,从而便于下一次继续向上调整
}
return ;
}
//从堆顶取出一个元素
int pop(){
int t;
t = h[1];//用一个临时变量及记录堆顶点的值
pos[t] = 0;//其实这句话要不要无所谓
h[1] = h[size]; //将堆的最后一个点赋值到堆顶
pos[h[1]] = 1;
size--;//将堆的元素减少一
siftdown(1);//向下调整1
return t;
}
int main(){
int n, m, i, j, k;
//u、v、w和next数组的大小要根据实际情况来设置,此图是无向图,要比2*m的最大值要大1
//first要比n的最大值要大1
int u[19], v[19], w[19], first[7], next[19];
int count = 0, sum = 0;//count用来记录生成树中顶点的个数,sum用来存储路径之和。
//读入n和m,n表示顶点个数,m表示边的个数
cin >> n >> m;
for (int i = 1; i <= m; ++i){
cin >> u[i] >> v[i] >> w[i];
}
//这里是无向图,所以需要将所有的边再反向存储一次
for (int i = m + 1; i <= 2 * m; ++i){
u[i] = v[i - m];
v[i] = u[i - m];
w[i] = w[i - m];
}
//开始使用邻接表存储边
for (int i = 1; i <= n; ++i){
first[i] = -1;
}
for (int i = 1; i <= 2*m; ++i){
next[i] = first[u[i]];
first[u[i]] = i;
}
//Prim核心部分开始
//将1号顶点加入生成树
book[1] = 1; //这里用book数组来标记一个顶点已经加入生成树
count ++;
//初始化dis数组,这里是1号顶点到其余各个顶点的初始距离
dis[1] = 0;
for (int i = 2; i <= n; ++i){
dis[i] = inf;
}
k = first[1];
while(k != -1){
dis[v[k]] = w[k];
k = next[k];
}
//初始化堆
size = n;
for (int i = 1; i <= size; ++i){
h[i] = i;
pos[i] = i;
}
for (i = size/2; i >= 1; --i) {siftdown(i);}
pop();//先弹出一个堆顶元素,因为此时堆顶是1号顶点。
while(count < n){
j = pop();
book[j] = 1;
count ++;
sum = sum + dis[j];
//扫描当前顶点j所有的边,再以j为中间节点,进行松弛
k = first[j];
while(k != -1){
if(book[v[k]] == 0 && dis[v[k]] > w[k]){
dis[v[k]] = w[k];//更新距离
siftup(pos[v[k]]);//对该点在堆中进行向上调整
//提示:pos【v[k]】存储的是顶点v[k]在堆中的位置
}
k = next[k];
}
}
cout << sum;
return 0;
}