最小生成树(MST)
生成树:所以顶点均由边连接在一起,但不存在回路的图
上图中甲图不是生成树,乙图是生成树
- 生成树是图的极小连通子图,去掉一条边则非连通
- 生成树的顶点个数和图的顶点个数相同
- 一个有n个顶点的连通图的生成树有n-1条边
- 在生成树中再加一条边必然形成回路
- 生成树中任意两个顶点间的路径是唯一的
构造图的生成树,可以通过DFS或者BFS进行构造,将遍历过的边作为生成树的边即可
那么什么是最小生成树,假如给这个无向图每个边一个权值(网),不同生成树边权值之和不同,在所有生成树中,使得各边权值之和最小的那颗生成树称为该网的最小生成树,也叫最小代价生成树
Prim算法
算法思想:
- 设 N=(V, E) 是连通网,TE 是 N 上最小生成树中边的集合。
- 初始令 U={ u 0 u_0 u0},( u 0 u_0 u0∈V),(TE={})。
- 在所有 u ∈U,v ∈ V-U的边 (u, v) ∈E 中,找一条代价最小的边 ( u 0 u_0 u0, v 0 v_0 v0)。
- 将 ( u 0 u_0 u0, v 0 v_0 v0) 并入集合 TE,同时 ( v 0 v_0 v0) 并入 U。
- 重复上述操作直至 (U=V) 为止,则 T=(V, TE)为 N 的最小生成树。
解释一下什么意思,就是有几个集合,V集合存储所有顶点,U集合存储在最小生成树中的顶点,V-U集合是所有不在最小生成树中的顶点,TE是最小生成树上边的集合,找一条从在最小生成树上的顶点到不在最小生成树上的顶点之间代价最小的一个边加入到TE集合,同时将选择的这个不在最小生成树上的顶点加入到U集合中,重复这个过程直到U=V
下图为一个具体示例
【例题】
给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible
。
给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。
由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。
输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含三个整数 u,v,w,表示点 u 和点 v 之间存在一条权值为 w 的边。
输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible
。
数据范围
1≤n≤500,
1≤m≤
1
0
5
10^5
105,
图中涉及边的边权的绝对值均不超过 10000。
输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6
import java.io.*;
import java.util.*;
public class Main {
static final int N = 510;
static final int MAX_NUM = 2147483647 / 2;
static int n, m;
static int[][] graph = new int[N][N];
static int[] distance = new int[N];
static boolean[] inSet = new boolean[N];
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] row1 = br.readLine().split(" ");
n = Integer.parseInt(row1[0]);
m = Integer.parseInt(row1[1]);
//初始化邻接矩阵
for(int i = 0; i <= n; i++) {
for(int j = 0; j <= n; j++) {
graph[i][j] = MAX_NUM;
}
}
for(int i = 0; i < m; i++) {
String[] data = br.readLine().split(" ");
int a = Integer.parseInt(data[0]);
int b = Integer.parseInt(data[1]);
int c = Integer.parseInt(data[2]);
graph[a][b] = Math.min(graph[a][b], c);
//无向图,添加反向边
graph[b][a] = graph[a][b];
}
int res = prim();
if(res == MAX_NUM) {
System.out.print("impossible");
} else {
System.out.print(res);
}
br.close();
}
static int prim() {
//初始化距离数组(距离数组表示的是到集合的最小距离)
Arrays.fill(distance, MAX_NUM);
//res为最小生成树的距离
int res = 0;
//n次迭代
for(int i = 0; i < n; i++) {
//恢复临时变量t的值为-1
int t = -1;
//寻找集合外到集合中点的最小距离
for(int j = 1; j <= n; j++) {
if(!inSet[j] && (t == -1 || distance[t] > distance[j])) {
t = j;
}
}
//判断t结点到集合的距离是否是无穷
//如果是无穷的话,说明无法构成最小生成树,直接返回
//前提是不是第一个点
if(i != 0 && distance[t] == MAX_NUM) {
return MAX_NUM;
}
//更新res,前提是不是第一个点
if(i != 0) {
res += distance[t];
}
//更新状态数组
inSet[t] = true;
//更新其他点到集合的最小距离
for(int j = 1; j <= n; j++) {
distance[j] = Math.min(distance[j], graph[t][j]);
}
}
return res;
}
}
Kruskal算法
- 设连通网 N = (V, E),令最小生成树初始状态为只有 n 个顶点而无边的非连通图 T=(V, { }),每个顶点自成一个连通分量。
- 在 E 中选取代价最小的边,若该边依附的顶点落在 T 中不同的连通分量上(即:不能形成环),则将此边加入到 T 中;否则,舍去此边,选取下一条代价最小的边。
- 依此类推,直至 T 中所有顶点都在同一连通分量上为止 。
通俗易懂地解释一下的话就是,在所有顶点中找权重最小的边,直到所有顶点连通
这里使用了并查集来判断添加边(v1,v2)是否会形成回路
【例题】
给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible
。
给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。
由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。
输入格式
第一行包含两个整数 n 和 m。
接下来 mm 行,每行包含三个整数 u,v,w,表示点 u 和点 v 之间存在一条权值为 w 的边。
输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible
。
数据范围
1≤n≤
1
0
5
10^5
105,
1≤m≤2∗
1
0
5
10^5
105,
图中涉及边的边权的绝对值均不超过 1000。
输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6
import java.io.*;
import java.util.*;
public class Main {
static class Edge {
public int a;
public int b;
public int w;
public Edge(int a, int b, int w) {
this.a = a;
this.b = b;
this.w = w;
}
}
static final int N = 100010;
static final int M = 200010;
static int n, m;
static int[] parent = new int[M];
static List<Edge> edges = new ArrayList<>();
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] row1 = br.readLine().split(" ");
n = Integer.parseInt(row1[0]);
m = Integer.parseInt(row1[1]);
while (m-- > 0) {
String[] data = br.readLine().split(" ");
int a = Integer.parseInt(data[0]);
int b = Integer.parseInt(data[1]);
int c = Integer.parseInt(data[2]);
edges.add(new Edge(a, b, c));
}
//对edges按照权重排序
edges.sort((o1, o2) -> o1.w - o2.w);
//初始化并查集
for(int i = 0; i <= n; i++) {
parent[i] = i;
}
int res = 0;
int cnt = 0;
for(int i = 0; i < edges.size(); i++) {
Edge edge = edges.get(i);
int a = edge.a;
int b = edge.b;
int w = edge.w;
int fa = find(a);
int fb = find(b);
if(fa != fb) {
parent[fa] = fb;
res += w;
cnt++;
}
}
if (cnt < n - 1) {
System.out.println("impossible");
} else {
System.out.println(res);
}
}
public static int find(int x) {
if(parent[x] == x) {
return x;
} else {
parent[x] = find(parent[x]);
}
return parent[x];
}
}
算法名 | 普里姆算法 | 克鲁斯卡尔算法 |
---|---|---|
算法思想 | 选择点 | 选择边 |
时间复杂度 | (O(n^2))(n 为顶点数) | (O(e log e))(e 为边数) |
适应范围 | 稠密图 | 稀疏图 |