一、问题
给定一张带边权的无向图G=(V,E),n=|V|,m=|E|。由V中全部n个顶点和E中n-1条边构成的无向连通子图被称为一颗生成树,而其中边权之和最小的生成树则是该无向图G的最小生成树,应该如何得到一张图G的最小生成树?
二、解析
首先,任意一颗最小生成树一定包含无向图中权值最小的边。
否则通过反证法,最小生成树不含权值最小的边,则可以把这条最小边加到树上,n点n条边必定会构成一个环,把该环上任意另一条边去掉,剩下的仍是联通的生成树,且树的权值总和小于此前的树,则此前的树不满足最小生成树的定义。
Prim算法可以利用这一结论,从一个点开始维护一直维护最小生成树的一部分,并寻找与已经进树的节点相连的未进树的节点构成的边中最小的边,把它加入最小生成树的一部分。
当所有点都联通时,由于保证了每次联通都是剩余边中最小的边,不存在有一条更小的边能替换选中的边的情况,该生成树一定是最小生成树。
三、设计
先确定1号节点默认是最小生成树的一部分
读图,存入点与点之间的边权信息
从1号节点开始扫描,与其相连的点中最短的边和该相连点加入生成树,重复上述步骤
当所有点都被加入之后,便形成了最小生成树
四、分析
由于每一个点都需要遍历加入生成树,且遍历过程中需要遍历剩余每一个点到生成树的最小边,故n个点需要n+(n-1)+(n-2)+……+2+1,为(n+1)*n/2次操作,即时间复杂度为O(n²)
因为要存储所有点两两之间的关系,用邻接矩阵存储的话空间复杂度为O(n²)
五、实例
六、代码
#include<stdio.h>
#include<stdlib.h>
#include<memory.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 1005;
//a数组存图,d数组维护选出x的最小边值,n是点数,m是边数,ans是最小生成树的边权总和
int a[N][N], d[N], n, m, ans;
//v[x]用来标记x是否已经被加入最小生成树
bool v[N];
void prim() {
memset(d, INF, sizeof(d));//初始无连接默认为无穷大
memset(v, 0, sizeof(v));
d[1] = 0;//加入第一个点,到自己的边值为0,其他值在主函数根据读图更新为到1点最近的距离
for (int i = 1; i <= n; i++){
int x = 0;
for (int j = 1; j <= n; j++) {//用j来遍历不是最小生成树里的点,找到离树最近的那个点的编号赋给x
if (!v[j] && (x == 0 || d[j] < d[x]))
x = j;
}
v[x] = 1;//x点加入最小生成树
printf("v%d\n", x);
for (int y = 1; y <= n; y++){//用y遍历不是最小生成树内的点,更新其到最小生成树的最短距离
if (!v[y])
d[y] = min(d[y], a[x][y]);//y点与最小生成树没联通还是无穷大,有联通要比较,可能是原来最短的,也可能是x加入后变短了
}
}
}
int main(void){
scanf("%d%d", &n, &m);
//构建邻接矩阵
memset(a, INF, sizeof(a));
for (int i = 1; i <= n; i++)
a[i][i] = 0;
for (int i = 1; i <= m; i++){
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
a[y][x] = a[x][y] = min(a[x][y], z);//考虑有两点间有两条路以上的情况
}
//求最小生成树
printf("加入顺序:\n");
prim();
for (int i = 2; i <= n; i++)
ans += d[i];
printf("最小生成树边权和为:\n%d\n", ans);
}
/*测试例子
6 10
1 2 6
1 3 1
1 4 5
2 3 5
2 5 3
3 4 5
3 5 6
3 6 4
4 6 2
5 6 6
*/