最小生成树
一、定义与背景
- 加权图:每条边关联一个权值(如距离、成本)的图。
- 生成树:图的无环连通子图,包含所有顶点。
- 最小生成树(MST):权值和最小的生成树。
- 应用:
- 电路(顶点:元器件,边:导线,权值:长度/时间)。
- 航空(顶点:机场,边:航线,权值:距离/费用)。
- 电力分配、图像分析等。
二、假设与约定
- 连通图:仅考虑连通图,非连通图生成“最小生成森林”。
- 权值特性:
- 不一定与距离成正比。
- 可为 0 或负数。
- 假设所有边权值不同(简化唯一性证明,实际算法适用等值情况)。
- 目标:在一幅加权连通无向图中找到 MST。
三、核心原理
1. 树的基本性质
2. 切分定理(Cut Property)
- 定义:
- 切分:将图顶点分为两个非空、不重叠的集合。
- 横切边:连接两个集合的边。
- 命题 J:任意切分的横切边中,权值最小的边必属于 MST。
- 证明(反证法):
- 假设 MST 不含最小横切边 ( e ),加入 ( e ) 形成环。
- 环中必有另一横切边 ( f )(权值 > ( e ))。
- 删除 ( f ),保留 ( e ),得到更小权值的生成树,与 MST 定义矛盾。
3. 贪心算法
- 命题 K:从灰色边开始,反复选择不产生黑色横切边的切分,将最小横切边标记为黑色,直到标记 ( V-1 ) 条边。
- 证明:切分定理保证每次选择的边属于 MST,( V-1 ) 条边形成连通无环图,即 MST。
四、数据结构
1. 加权边(Edge
)
public class Edge implements Comparable<Edge> {
private final int v, w; // 顶点
private final double weight; // 权值
public Edge(int v, int w, double weight) { this.v = v; this.w = w; this.weight = weight; }
public double weight() { return weight; }
public int either() { return v; }
public int other(int vertex) { return vertex == v ? w : vertex == w ? v : throw new Exception(); }
public int compareTo(Edge that) { return Double.compare(this.weight, that.weight); }
}
2. 加权无向图(EdgeWeightedGraph
)
public class EdgeWeightedGraph {
private final int V; // 顶点数
private int E; // 边数
private Bag<Edge>[] adj; // 邻接表
public EdgeWeightedGraph(int V) {
this.V = V; this.E = 0;
adj = (Bag<Edge>[]) new Bag[V];
for (int v = 0; v < V; v++) adj[v] = new Bag<>();
}
public void addEdge(Edge e) { int v = e.either(), w = e.other(v); adj[v].add(e); adj[w].add(e); E++; }
public Iterable<Edge> adj(int v) { return adj[v]; }
public Iterable<Edge> edges() {
Bag<Edge> b = new Bag<>();
for (int v = 0; v < V; v++) for (Edge e : adj[v]) if (e.other(v) > v) b.add(e);
return b;
}
}
五、算法实现
1. Prim 算法
- 思想:从单一顶点开始,逐步扩展树,每次添加权值最小的横切边。
- 两种实现:
- 延时(Lazy)版本:
- 使用优先队列保存所有横切边(含失效边)。
- 时间:( O(E \log E) ),空间:( O(E) )。
java
public class LazyPrimMST {
private boolean[] marked; // MST 顶点
private Queue<Edge> mst; // MST 边
private MinPQ<Edge> pq; // 横切边
public LazyPrimMST(EdgeWeightedGraph G) {
pq = new MinPQ<>();
marked = new boolean[G.V()];
mst = new Queue<>();
visit(G, 0);
while (!pq.isEmpty()) {
Edge e = pq.delMin();
int v = e.either(), w = e.other(v);
if (marked[v] && marked[w]) continue; // 失效边
mst.enqueue(e);
if (!marked[v]) visit(G, v);
if (!marked[w]) visit(G, w);
}
}
private void visit(EdgeWeightedGraph G, int v) {
marked[v] = true;
for (Edge e : G.adj(v)) if (!marked[e.other(v)]) pq.insert(e);
}
public Iterable<Edge> edges() { return mst; }
}
- 即时(Eager)版本:
- 使用索引优先队列,仅保存每个非树顶点到树的最小边。
- 时间:( O(E \log V) ),空间:( O(V) )。
java
public class PrimMST {
private Edge[] edgeTo; // 到树最近的边
private double[] distTo; // 边的权值
private boolean[] marked; // MST 顶点
private IndexMinPQ<Double> pq; // 有效横切边
public PrimMST(EdgeWeightedGraph G) {
edgeTo = new Edge[G.V()];
distTo = new double[G.V()];
marked = new boolean[G.V()];
pq = new IndexMinPQ<>(G.V());
for (int v = 0; v < G.V(); v++) distTo[v] = Double.POSITIVE_INFINITY;
distTo[0] = 0.0; pq.insert(0, 0.0);
while (!pq.isEmpty()) visit(G, pq.delMin());
}
private void visit(EdgeWeightedGraph G, int v) {
marked[v] = true;
for (Edge e : G.adj(v)) {
int w = e.other(v);
if (marked[w]) continue;
if (e.weight() < distTo[w]) {
edgeTo[w] = e; distTo[w] = e.weight();
if (pq.contains(w)) pq.changeKey(w, distTo[w]);
else pq.insert(w, distTo[w]);
}
}
}
}
2. Kruskal 算法
- 思想:按边权值从小到大处理,加入不形成环的边,直到 ( V-1 ) 条边。
- 实现:
- 使用优先队列排序边,Union-Find 判断环。
- 时间:( O(E \log E) ),空间:( O(E) )。
java
public class KruskalMST {
private Queue<Edge> mst;
public KruskalMST(EdgeWeightedGraph G) {
mst = new Queue<>();
MinPQ<Edge> pq = new MinPQ<>();
for (Edge e : G.edges()) pq.insert(e);
UF uf = new UF(G.V());
while (!pq.isEmpty() && mst.size() < G.V() - 1) {
Edge e = pq.delMin();
int v = e.either(), w = e.other(v);
if (uf.connected(v, w)) continue;
uf.union(v, w);
mst.enqueue(e);
}
}
public Iterable<Edge> edges() { return mst; }
}
六、性能分析
算法 |
空间 |
时间 |
Lazy Prim |
( E ) |
( E \log E ) |
Eager Prim |
( V ) |
( E \log V ) |
Kruskal |
( E ) |
( E \log E ) |
- Prim:适合稠密图(即时版本更高效)。
- Kruskal:适合稀疏图,边排序为主瓶颈。
七、展望
- 历史:Prim(1939/1961)、Kruskal(1956)、Boruvka(1926)。
- 优化:斐波纳契堆(( E + V \log V ))、Chazelle(接近线性但复杂)。
- 线性时间:理论上未证明不存在,实际接近线性。
八、总结
- 核心:切分定理和贪心策略。
- 算法:Prim(逐步扩展树)、Kruskal(合并森林)。
- 适用性:已解决大多数实际问题,现代数据结构提升效率。