一、简介
最小生成树就是将 n 个顶点, n - 1 条边,通过一个连接起来,且使权值最小的一种结构。
换句话来说,就是给定一个无向图,在图中选择若干条边把图中的所有节点连接起来,要求边长之和最小。在图论中,叫做求最小生成树。
主要的应用:
比如要在 n 个城市之间铺设光缆,使得这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,所以我们的目标要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。
主要算法有 Prim算法 和 Kruskal算法。
下面通过一道Acwing上的一道最小生成树例题来分别简单介绍这两种算法。
二、Prim算法
可理解为 “加点法”, 每次迭代找到不在连通块中的距离最近的点,加入到连通块中,将连通块逐渐扩大,最后将整个图连通起来,并且边长之和最小。
1、先把所有点之间的距离初始化为正无穷
2、n次迭代,找到不在集合当中的最小的点,这个集合指当前已经在连通块中的所有点,找到该点赋给 t ,用 t 更新其他点到集合的距离,再把 t 加到集合当中去。
3、AC代码如下:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int n, m;//n 个点,m 条边
int g[N][N];//存储图
int dist[N];//存储各个点到生成树的距离
bool st[N];//判断点是否被加入到生成树中
//prim算法核心代码
int prim()
{
//初始化距离数组为一个很大的数(10亿左右)
memset(dist, 0x3f, sizeof(dist));
int res = 0;//最小生成树所有边的长度之和
//每次循环选出一个点加入到生成树
for(int i = 0; i < n; i++){
//初始化为-1,表示没有找到任何一个点
int t = -1;
for(int j = 1; j <= n; j++){
//如果没有在树中,且到树的距离最短,则选择该点
if(!st[j] && (t == -1 || dist[j] < dist[t]))
t = j;//将该点赋给t
}
//一定要先累加,再进行更新生成树
//遍历所有点,找不到连通的其他点,即不存在最小生成树
if (i && dist[t] == INF)
return INF;
if (i)//把找到的符合条件的点的长度加上
res += dist[t];
//更新生成树,其他的点到生成树的最短距离
for (int j = 1; j <= n; j++)
dist[j] = min(dist[j], g[t][j]);
//加入生成树中,标记
st[t] = true;
}
return res;
}
int main()
{
//各个点之间的距离初始化成无穷大
memset(g, 0x3f, sizeof(g));
cin >> n >> m;
while(m --){
int a, b, w;
cin >> a >> b >> w;
//存储两点之间最小的权重
g[a][b] = g[b][a] = min(g[a][b], w);
}
int t = prim();
//如果t为无穷大,则代表找不到符合条件的点,无法构成最小生成树
if (t == INF)
puts("impossible");
else
printf("%d\n", t);
return 0;
}
三、Kruskal算法
可理解为 “加边法”,最初最小生成树的边数为 0,每次迭代选择一条不在集合内的权值最短的边,加入到集合中,组成最小生成树。
1、使用快排将所有边按权值从小到大排序。时间复杂度为 O(log n).
2、枚举每组边 a 、b,权重 c ,如果 a、b不连通,就将这条边加入集合中,直到具有 n 个顶点的连通块筛选出来 n-1 条边为止。时间复杂度为 O(n) .
3、判断 a、b是否连通的方法为:使用并查集。
- 初始化各个顶点在不同的集合中,父节点为它自己。
- 按快排的从小到大的顺序遍历每条边,判断这条边的两个顶点是否有相同的父节点,如果有那就使在同一个集合中。
- 如果该条边上的两个顶点在一个集合中,说明两个顶点已经连通,这条边不要。如果不在一个集合中,则加入这条边到集合中,连通这两个顶点。
总的时间复杂度为 O(n log n).
4、AC代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 200010;
int n, m;
int p[N];
//结构体存储 两点及其权值
struct Edge
{
int a, b, w;
//重载小于号,因为再给边排序的时候是按照边的权重进行排序的,这样当两个边进行比较的时候就会使用他们的权重进行比较了
bool operator < (const Edge &W)const
{
return w < W.w;
}
}edges[N];
//查找根节点
int find(int x){
if (p[x] != x)
p[x] = find(p[x]);
return p[x];
}
int main()
{
scanf("%d%d", &n, &m);
//初始化
for (int i = 0; i < m; i++){
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};
}
//快排,将所有边的权值从小到大排序
sort(edges, edges + m);
//初始化并查集
for (int i = 1; i <= n; i++)
p[i] = i;
后面这部分就是"Kruskal算法"的核心代码
int res = 0;//res存的是最小生成树的所有边的权值
int cnt = 0;//cnt存的是当前加入的边数
for (int i = 0; i < m; i++){
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if (a != b){
p[a] = b;//将 a,b 所在的两个集合连接起来
res += w;//集合中的总权重加上这条边的权重
cnt++;//因为加入的是a-b这一条边,将a,b所在的两个集合连接之后,总的边数+1
}
}
//只有当 cnt == n - 1 时才能表示已经将所有点加入到集合中,可以生成最小生成树
if (cnt < n - 1)
puts("impossible");
else
printf("%d\n", res);
return 0;
}
四、小结
1、点多边少,即稀疏图,一般选用 Prim算法, 采用 邻接矩阵 存储边之间的关系。
2、点少边多,即稠密图,一般选用 Kruskal算法,采用 邻接表 存储边之间的关系,但更简单更常用的是用 结构体 的方式来存储。