目录
[1. Prim 算法](#1. Prim 算法)
[2. Kruskal 算法](#2. Kruskal 算法)
一、概念介绍
在无向连通加权图 中,生成树是一个包含所有顶点 的无环子图 。 如果我们为每条边赋予权重(通常表示成本、距离、时间等),则最小生成树 是所有生成树中权重之和最小的那棵树。
最小生成树的性质:
-
顶点数固定:MST包含图中所有的顶点。
-
边数为 n-1:对于 n 个顶点的连通无向图,生成树恰好有 n-1 条边。
-
无环性:MST中不允许出现环。
-
最小权重:总权重在所有生成树中是最小的。
应用场景:
-
网络设计:构建通信网络、电网、计算机网络时,最小化建设成本。
-
道路规划:最小化公路、铁路或管道的建设长度。
-
聚类分析:在数据聚类中用MST来发现数据的结构。
-
图像处理:用于图像分割等计算机视觉任务。
-
物流配送:寻找最经济的配送路径(不是最短路径问题,而是覆盖所有地点的最小代价)。
概念理解:
-
节点(Node) 图中的顶点,代表实体(例如城市、网络设备、仓库等)。
-
边(Edge) 图中连接两个顶点的线段,带有权重(例如距离、费用、延迟等)。
-
权重(Weight) 边的数值属性,用于衡量连接两个节点的"成本"。
-
无向图 边的方向不区分"起点"和"终点",即边
(u, v)
与(v, u)
等价。 -
连通性 从任意节点可以到达其他任意节点的图称为连通图。
-
无环性 图中不存在回路。生成树一定是无环的。
-
并查集(Union-Find) 一种用于检测两个顶点是否属于同一个连通分量的数据结构,常用于 Kruskal 算法中避免形成环。
二、两种典型的MST算法
1. Prim 算法
-
算法思想
Prim 算法从某个起始节点出发,不断选择权重最小且能连接新的未访问节点的边,直到所有节点都被包含进来。 其过程类似于"从一个点出发,不断扩展到最近的未访问节点"。
-
核心步骤
-
从任意一个顶点开始,将其加入已访问集合。
-
将该顶点的所有边加入优先队列(按权重排序)。
-
从优先队列中取出权重最小的边,如果它连接的节点未被访问,则将该节点加入已访问集合,并将其边加入优先队列。
-
重复步骤3,直到所有顶点都被访问或队列为空。
-
Java实现(Prim):
package graph;
import java.util.*;
/**
* 无向图 无环 连通
*/
public class MinimumSpanningTrees {
/**
* prim算法求解最小生成树
*
* @param nodes
*/
public static List<Edge> primMST(List<Node> nodes) {
if (nodes.isEmpty()) return Collections.emptyList();
List<Edge> mst = new ArrayList<>();
PriorityQueue<Edge> edgeQueue = new PriorityQueue<>(Comparator.comparingInt(e -> e.weight)); // 存储访问过的节点集合对应的边集合,并且按照权重排序
Set<Node> visited = new HashSet<>(); // 存储访问过的节点集合
// 从第一个节点开始
Node startNode = nodes.get(3);
visited.add(startNode);
edgeQueue.addAll(startNode.adj);
while (!edgeQueue.isEmpty() && visited.size() < nodes.size()) {
Edge minEdge = edgeQueue.poll();
Node nextNode = minEdge.to;
if (!visited.contains(nextNode)) {
visited.add(nextNode);
mst.add(minEdge);
// 添加新节点的所有边到优先队列
for (Edge edge : nextNode.adj) {
if (!visited.contains(edge.to)) {
edgeQueue.add(edge);
}
}
}
}
return mst;
}
public static void main(String[] args) {
Node v1 = new Node("v1");
Node v2 = new Node("v2");
Node v3 = new Node("v3");
Node v4 = new Node("v4");
Node v5 = new Node("v5");
Node v6 = new Node("v6");
Node v7 = new Node("v7");
v1.addEdge(v2,2);
v1.addEdge(v3,4);
v1.addEdge(v4,1);
v2.addEdge(v1,2);
v2.addEdge(v5,10);
v2.addEdge(v4,3);
v3.addEdge(v1,4);
v3.addEdge(v4,2);
v3.addEdge(v6,5);
v4.addEdge(v1,1);
v4.addEdge(v2,3);
v4.addEdge(v3,2);
v4.addEdge(v5,7);
v4.addEdge(v6,8);
v4.addEdge(v7,4);
v5.addEdge(v2,10);
v5.addEdge(v4,7);
v5.addEdge(v7,6);
v6.addEdge(v3,5);
v6.addEdge(v4,8);
v6.addEdge(v7,1);
v7.addEdge(v4,4);
v7.addEdge(v5,6);
v7.addEdge(v6,1);
List<Node> nodes = Arrays.asList(v1, v2, v3, v4, v5, v6, v7);
List<Edge> edges = primMST(nodes);
System.out.println("Total weight: " + edges.stream().map(item -> item.weight).reduce(0, Integer::sum));
for(Edge edge:edges){
System.out.println(edge.from.name + "->" + edge.to.name + ":" + edge.weight);
}
}
}
-
执行结果
Total weight: 16 v4->v1:1 v4->v3:2 v1->v2:2 v4->v7:4 v7->v6:1 v7->v5:6
2. Kruskal 算法
-
算法思想
Kruskal 算法是一种"边驱动"策略,按权重升序对所有边排序 ,然后依次选择最小的边加入生成树,但前提是不会形成环。 使用并查集判断加入边后是否形成环。
-
核心步骤
-
将所有边按权重升序排序。
-
初始化并查集,每个节点是一个独立集合。
-
依次取最小的边,若该边的两个端点属于不同集合,则将其加入MST,并合并两个集合。
-
当MST中边数达到
n-1
时结束。
-
Java实现(Kruskal):
package graph;
import java.util.*;
/**
* 无向图 无环 连通
*/
public class MinimumSpanningTrees {
/**
* Kruskal算法求解最小生成树
*
* @param nodes 图中的所有节点
* @return 最小生成树的边集合
*/
public static List<Edge> kruskalMST(List<Node> nodes) {
if (nodes.isEmpty()) return Collections.emptyList();
List<Edge> mst = new ArrayList<>();
// 收集所有边,按权重从小到大排序
List<Edge> allEdges = new ArrayList<>();
for (Node node : nodes) {
allEdges.addAll(node.adj);
}
allEdges.sort(Comparator.comparingInt(e -> e.weight));
// 使用【并查集】来检测环
UnionFind uf = new UnionFind(nodes);
for (Edge edge : allEdges) {
Node from = edge.from;
Node to = edge.to;
if (!uf.isConnected(from, to)) { // 判断两个集合能否联通
mst.add(edge);
uf.union(from, to);
}
// 如果已经连接了所有节点,提前退出
if (mst.size() == nodes.size() - 1) {
break;
}
}
return mst;
}
public static void main(String[] args) {
Node v1 = new Node("v1");
Node v2 = new Node("v2");
Node v3 = new Node("v3");
Node v4 = new Node("v4");
Node v5 = new Node("v5");
Node v6 = new Node("v6");
Node v7 = new Node("v7");
v1.addEdge(v2,2);
v1.addEdge(v3,4);
v1.addEdge(v4,1);
v2.addEdge(v1,2);
v2.addEdge(v5,10);
v2.addEdge(v4,3);
v3.addEdge(v1,4);
v3.addEdge(v4,2);
v3.addEdge(v6,5);
v4.addEdge(v1,1);
v4.addEdge(v2,3);
v4.addEdge(v3,2);
v4.addEdge(v5,7);
v4.addEdge(v6,8);
v4.addEdge(v7,4);
v5.addEdge(v2,10);
v5.addEdge(v4,7);
v5.addEdge(v7,6);
v6.addEdge(v3,5);
v6.addEdge(v4,8);
v6.addEdge(v7,1);
v7.addEdge(v4,4);
v7.addEdge(v5,6);
v7.addEdge(v6,1);
List<Node> nodes = Arrays.asList(v1, v2, v3, v4, v5, v6, v7);
List<Edge> edges = kruskalMST(nodes);
System.out.println("Total weight: " + edges.stream().map(item -> item.weight).reduce(0, Integer::sum));
for(Edge edge:edges){
System.out.println(edge.from.name + "->" + edge.to.name + ":" + edge.weight);
}
}
}
- 并查集(Union-Find)实现
/**
* 并查集
*/
class UnionFind {
private Map<Node, Node> parent; // 构造森林
private Map<Node, Integer> rank;
public UnionFind(List<Node> nodes) {
parent = new HashMap<>();
rank = new HashMap<>();
for (Node node : nodes) {
parent.put(node, node);
rank.put(node, 0);
}
}
public Node find(Node node) {
if (parent.get(node) != node) {
parent.put(node, find(parent.get(node))); // 递归路径压缩
}
return parent.get(node);
}
public boolean isConnected(Node x, Node y) {
return find(x) == find(y);
}
public void union(Node x, Node y) {
Node rootX = find(x);
Node rootY = find(y);
if (rootX == rootY) return;
// 按秩合并
if (rank.get(rootX) > rank.get(rootY)) {
parent.put(rootY, rootX);
} else if (rank.get(rootX) < rank.get(rootY)) {
parent.put(rootX, rootY);
} else {
parent.put(rootY, rootX);
rank.put(rootX, rank.get(rootX) + 1);
}
}
}
三、两种算法对比
算法 | 思路 | 数据结构 | 复杂度 | 适用场景 |
---|---|---|---|---|
Prim | 从点出发扩展最小边 | 优先队列 + 集合 | O(E log V) | 稠密图 |
Kruskal | 从小边开始构建树 | 排序 + 并查集 | O(E log E) | 稀疏图 |
-
Prim 更适合稠密图(边很多),因为它每次只扩展到一个新的节点。
-
Kruskal 更适合稀疏图(边少),因为它先全局排序再选择边