Day10:最小生成树(Minimum Spanning Tree, MST)入门
常用的两种最小生成树算法:克鲁斯卡尔(Kruskal)算法、普利姆(Prim)算法。
零、什么是最小生成树:
假定有一个含n个顶点的边带权的无向图,其最小生成树具有如下特征:
1、是无向图的子图;
2、包含无向图的全部n个顶点,具有n-1条边(没有回路);
3、最小生成树包含的所有边的权重和是满足前两条特征的所有子图(这种子图称为生成树)中最小的。
一、Kruskal算法:
1、基本思想:
1)、将生成树初始化为含有n个顶点,没有边的非连通图(将所有节点看成一片森林)。
2)、从原无向图中选择权值最小的边,若该边两端的顶点不在同一个连通块中(两端的顶点不都在生成树中),则将此边作为生成树的一条边;否则舍去此边,寻找其他边中权值最小的边,重复验证直至找到。
3)、不断地向生成树中添加权值较小的边,直至生成树含有n-1条边。此时,生成树应该满足前面提到的最小生成树的特征,换句话说,此时已经构建出了原无向图的最小生成树。
2、实现方法:
从基本思想可以看出,构造最小生成树就是寻找边权值之和最小,包括原无向图所有顶点的一个连通块。因此,可以使用并查集进行处理。
我的并查集笔记
3、示例代码:
#include <bits/stdc++.h>
using namespace std;
typedef struct edge_s {
// 定义表示边的结构体
int u, v, w; // 表示顶点u,v之间的边,权值w
bool friend operator< (const edge_s &x, const edge_s &y) {
return x.w < y.w; // 重载<运算符, 按权重升序排序
}
}edge;
const int MAXN = 100 + 10;
int f[MAXN]; // 存储并查集
edge e[MAXN]; // 存储边
int find(int node) {
// 用并查集查找某顶点对应的代表元
return node == f[node] ? node : f[node] = find(f[node]);
}
int Kruskal(int n, int m) {
// 对有n个节点, m条无向边的图构建最小生成树
// 返回最小生成树的总权值, 构建失败返回-1
sort(e, e + m); // 按权升序排序所有边
for(int i = 0; i < n; ++i) f[i] = i; // 初始化并查集
int ans = 0; // 生成树的总权值
int num = 0; // 已使用的边数
for(int i = 0; i < m; ++i) { // 遍历所有边
int f1 = find(e[i].u);
int f2 = find(e[i].v);
if(f1 != f2) { // 判断u,v是否已连接
f[f2] = f1;
ans += e[i].w;
++num;
}
if(num == n - 1) break; // 已经构建出最小生成树
}
if(num != n - 1) return -1; // 遍历所有边后仍无法构建出最小生成树则构建失败
else return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(NULL); cout.tie(NULL);
int n, m;
cin >> n >> m; // n个顶点 m条边
for(int i = 0; i < m; ++i) // 读入m条边
cin >> e[i].u >> e[i].v >> e[i].w;
int weight = Kruskal(n, m); // 调用示例
cout << weight << endl;
return 0;
}
二、Prim算法:
1、基本思想:
1)、在原无向图中选择一个顶点,作为初始的生成树。
2)、在被选择的顶点连接的所有边中选择一条权值最小的边,将该边和该边另一端的顶点加入到生成树中。
3)、在所有连接树中顶点与树外顶点的边中选择权值最小的边,将选出的边和对应顶点加入生成树中。重复操作直到所有顶点都加入树中。
2、实现方法:贪心思想。
3、示例代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 100 + 10;
const int inf = 0x3f3f3f3f;
int edge[MAXN][MAXN];
// edge[i][j]存储顶点i到顶点j的边的权值, 边不存在置为inf, 某个顶点到其自身的权值置为0
int used[MAXN]; // used[i]表示顶点i是否已在生成树中
int weight[MAXN]; // weight[i]存储当前生成树与顶点i的最小权值
int Prim(int n, int m) {
// n个顶点, m条边, 注意顶点编号为1~n
// 返回最小生成树总权值, 失败返回-1
memset(used, 0, sizeof(used));
int index = 1; // 当前加入到生成树的顶点
used[index] = 1;
for(int i = 1; i <= n; ++i) weight[i] = edge[index][i];// 更新这个点到其他点的权值
int ans = 0; // 存放总权值
for(int i = 1; i < n; ++i) { // 遍历剩下的n-1个顶点
int minw = inf;
for(int j = 1; j <= n; ++j) { // 找出未加入生成树且边权值最小的点
if(!used[j] && weight[j] < minw) {
minw = weight[j];
index = j;
}
}
if(minw == inf) return -1; // 如果没有找到, 说明不存在最小生成树
ans += minw; // 累加权值
used[index] = 1; // 将这个点加入最小生成树中
for(int j = 1; j <= n; ++j) { // 更新这个点加入后,当前这棵小树到未加入的点的最小权值
if(!used[j] && weight[j] > edge[index][j]) weight[j] = edge[index][j];
}
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(NULL); cout.tie(NULL);
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; ++i) { // 边权值的初始化, 顶点编号1~n
for(int j = 1; j <= n; ++j) {
if(i == j) edge[i][j] = 0;
else edge[i][j] = inf;
}
}
for(int i = 0, u, v, w; i < m; i++) { //读入m条边
cin >> u >> v >> w;
edge[u][v] = edge[v][u] = min(w, edge[u][v]); // 消除重边的影响
}
cout << Prim(n, m) << endl;
return 0;
}