最小生成树问题描述
最小生成树(Minimum Spanning Tree, MST)问题是图论中的一个经典问题,它在各种网络设计领域都有广泛的应用。最小生成树指的是在一个加权连通图中,找到一个边的子集,使得这些边构成的树包含图中的所有顶点,并且边的权重之和最小。
给定连通图,求最小生成树:
该连通图的一个最小生成树:
Prim求最小生成树
prim算法是基于贪心
的,他的核心思想:
- 选择一个距离集合最近的点,加入集合
- 更新其他点到集合的距离
- 回到步骤1
可以看到算法的过程就是遍历所有的点,每次选择一个距离集合最近的点,加入集合,并更新其他点到集合的距离,因此是两层循环,时间复杂度是O(n^2)。
示例图演示起来太麻烦了,可以自己根据prim算法的步骤演示一遍
示例代码:
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n, m; // 连通图中的点 以及边的个数
n = in.nextInt();
m = in.nextInt();
int[][] graph = new int[n + 1][n + 1]; // 邻接矩阵存储图
int[] dist = new int[n + 1]; // 距离集合的距离
int[] st = new int[n + 1]; // 是否在集合中
Arrays.fill(dist, Integer.MAX_VALUE);
for (int i = 0; i <= n; i++) Arrays.fill(graph[i], Integer.MAX_VALUE);
for (int i = 0; i < m; i++) {
int a, b, c;
a = in.nextInt();
b = in.nextInt();
c = in.nextInt();
graph[a][b] = graph[b][a] = Math.min(graph[a][b], c);
}
int ans = 0; // 最小生成树的权重之和
boolean flag = true;
// 加入n次
for (int i = 0; i < n; i++) {
int t = -1;
for (int j = 1; j <= n; j++) {
if (st[j] == 0 && (t == -1 || dist[j] < dist[t])) t = j;
}
if (i != 0 && dist[t] == Integer.MAX_VALUE) {
flag = false;;
break;
}
if (i != 0) ans += dist[t];
st[t] = 1;
for (int j = 1; j <= n; j++) {
dist[j] = Math.min(dist[j], graph[t][j]);
}
}
if (flag) {
System.out.println(ans);
} else {
System.out.println("impossible");
}
}
}
Kruskal求最小生成树
该算法也是基于贪心
的,基本思想是从权重最小的边开始依次加入到集合中。
为了避免出现重边,需要用到并查集
来判断两个点是否已经在树中了。
示例代码:
import java.util.*;
class Main{
public static void main(String[] args) {
int n, m;
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
List<int[]> edges = new ArrayList<>();
for (int i = 0; i < m; i++) {
int a, b, c;
a = in.nextInt();
b = in.nextInt();
c = in.nextInt();
edges.add(new int[]{a, b, c});
}
Collections.sort(edges, (a, b) ->(a[2] - b[2]));
int[] father = new int[n + 1];
for (int i = 0; i <= n; i++) father[i] = i;
int cnt = 0; // 构成最小生成树时 cnt = n - 1
int res = 0; // 权重
for (int i = 0; i < edges.size(); i++) {
int[] cur = edges.get(i);
int a = cur[0];
int b = cur[1];
int c = cur[2];
int fathera = find(a, father);
int fatherb = find(b, father);
if (fathera != fatherb) {
cnt++;
res += c;
father[fathera] = father[fatherb];
}
}
if (cnt != n - 1) {
System.out.println("impossible");
} else {
System.out.println(res);
}
}
public static int find(int u, int[] father) {
if (father[u] != u) {
father[u] = find(father[u], father);
}
return father[u];
}
}
把连通图看作一个点集合,kruskal算法的过程就是往集合中加边的过程。
总结
prim遍历点,kruskal遍历边,所以一个适合稠密图,一个适合稀疏图。
其实这两个算法不难,关键是做题的时候能不能想到。