Algorithms Fourth Edition
Written By Robert Sedgewick & Kevin Wayne
Translated By 谢路云
Chapter 4 Section 3 最小生成树
定义
树是特殊的图
图的生成树: 含有图全部顶点的无环连通子图
加权无向图的最小生成树(MST):权重最小的生成树
约定
只考虑连通图:根据生成树的定义
边的权重可以为0或者为负
所有边的权重各不相同:方便证明
原理
切分定理
切分:将图的顶点集分为两个非空并且没有交集的集合
横切边:链接两个属于不同集合的顶点的边。(下图的红色边)
在一副加权图中,给定任意的切分,它的横切边中的权重最小者必然属于图中的最小生成树。
贪心算法
将含有V个顶点的任意加权连通图中属于最小生成树的边标记为黑色。
初始状态下所有边均为灰色,找到一种切分,它产生的横切边均不为黑色。
将它权重最小的横切边标记为黑色。
反复,直到标记了V-1条黑色边为止。
加权无向图
加权边API
边的两个顶点
权重
权重大小比较
Edge 代码
public class Edge implements Comparable<Edge> {
private final int v; // one vertex
private final int w; // the other vertex
private final double weight; // edge 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) {
if (vertex == v)
return w;
else if (vertex == w)
return v;
else
throw new RuntimeException("Inconsistent edge");
}
public int compareTo(Edge that) {
if (this.weight() < that.weight())
return -1;
else if (this.weight() > that.weight())
return +1;
else
return 0;
}
public String toString() {
return String.format("%d-%d %.2f", v, w, weight);
}
}
加权无向图API
修改了方法 Iterable<Edge> adj(v) 原来返回的是相连的点,现在返回的是邻边
新增了方法 Iterable<Edge> edges() 因为最小生成树更看重边的要素
EdgeWeightedGraph 代码
API允许平行边和自环,但是下面代码在实现的过程中并没有统计它们,这对最小生成树并不会产生影响。
public class EdgeWeightedGraph {
private final int V; // number of vertices
private int E; // number of edges
private Bag<Edge>[] adj; // adjacency lists
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<Edge>();
}
public int V() {
return V;
}
public int E() {
return E;
}
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<Edge>();
for (int v = 0; v < V; v++)
for (Edge e : adj[v])
if (e.other(v) > v) //保证不重复
b.add(e);
return b;
}
}
最小生成树API
Prim算法
从点的方面考虑构建一颗MST
设图G顶点集合为U,
任意选择图G中的一点作为起始点a,将该点加入集合V
再从集合U-V中找到另一点b使得点b到V中任意一点的权值最小,此时将b点也加入集合V
以此类推,直至所有顶点全部被加入V,此时就构建出了一颗MST。
因为有N个顶点,所以该MST就有N-1条边,每一次向集合V中加入一个点,就意味着找到一条MST的边。
数据结构
顶点: 使用boolean marked[]。如果顶点v在树中,则marked[v]=true。
边: 使用队列来保存最小生成树的边 or 由顶点索引的数组edgeTo[]
横切边: 使用优先队列MinPQ<Edge>来根据权重进行比较
延时Prim图示
将每一条和树相连的边加入优先队列MinPQ<Edge>,依次取出最小的边,取出后再判断是否是横切边
LazyPrimMST 代码
-
复杂度
时间:ElogE 最坏情况下,一次插入成本为~lgE,删除最小元素的成本为~2lgE,最多只能插入E条边,删去E条边。
空间:E 优先队列中最多可能有E条边
public class LazyPrimMST {
private boolean[] marked; // MST vertices
private Queue<Edge> mst; // MST edges
private MinPQ<Edge> pq; // crossing (and ineligible) edges 横切边
public LazyPrimMST(EdgeWeightedGraph G) {
pq = new MinPQ<Edge>();
marked = new boolean[G.V()];
mst = new Queue<Edge>();
visit(G, 0); // assumes G is connected (see Exercise 4.3.22)
while (!pq.isEmpty()) {
Edge e = pq.delMin(); // Get lowest-weight
int v = e.either(), w = e.other(v); // edge from pq.
if (marked[v] && marked[w])
continue; // Skip if ineligible.
mst.enqueue(e); // Add edge to tree.
if (!marked[v])
visit(G, v); // Add vertex to tree
if (!marked[w])
visit(G, w); // (either v or w).
}
}
private void visit(EdgeWeightedGraph G, int v) { // Mark v and add to pq all edges from v unmarked vertices.
marked[v] = true;
for (Edge e : G.adj(v))
if (!marked[e.other(v)])
pq.insert(e);
}
public Iterable<Edge> edges()
{ return mst; }
public double weight() // See Exercise 4.3.31.
}
即时Prim图示
将每个点和树相连的最小边维护在数组edgeTo[] 和 distTo[]里,每向树加入一个点,更新一次数组
edgeTo[] 记录顶点/边; distTo[] 记录权重
PrimMST 代码
-
复杂度
时间:ElogV 最坏情况下,一次插入成本为~lgV,删除最小元素的成本为~2lgV,最多只能插入E条边,删去E条边。
空间:E 优先队列中最多可能有E条边
public class PrimMST {
private Edge[] edgeTo; // shortest edge from tree vertex
private double[] distTo; // distTo[w] = edgeTo[w].weight()
private boolean[] marked; // true if v on tree
private IndexMinPQ<Double> pq; // eligible crossing edges 横切边
public PrimMST(EdgeWeightedGraph G) {
edgeTo = new Edge[G.V()];
distTo = new double[G.V()];
marked = new boolean[G.V()];
for (int v = 0; v < G.V(); v++)
distTo[v] = Double.POSITIVE_INFINITY; //初始化
pq = new IndexMinPQ<Double>(G.V());
distTo[0] = 0.0;
pq.insert(0, 0.0); // Initialize pq with 0, weight 0.
while (!pq.isEmpty())
visit(G, pq.delMin()); // Add closest vertex to tree.
}
private void visit(EdgeWeightedGraph G, int v) { // Add v to tree; update data structures.
marked[v] = true;
for (Edge e : G.adj(v)) {
int w = e.other(v);
if (marked[w]) //v w都在树里,不是横切边,因此不用更新
continue;
if (e.weight() < distTo[w]) { // 是横切边,且权重更小,因此更新
edgeTo[w] = e;
distTo[w] = e.weight();
if (pq.contains(w)) //顶点已存在,则修改
pq.change(w, distTo[w]);
else //顶点第一次出现,则新建
pq.insert(w, distTo[w]);
}
}
}
public Iterable<Edge> edges() // See Exercise 4.3.21.
public double weight() // See Exercise 4.3.31.
}
Kruskal算法
按照边的权重顺序(从小到大)处理
选择最小权重的边,判断是否会构成环,不会则加入最小生成树。
循环如此,直至树中含有V-1条边为止。
Kruskal算法图示
KruskalMST 代码
-
复杂度
时间:ElogE 初始化优先队列,最坏情况E次比较;每次操作成本2lgE次比较,最多还会多E次connected() 和 V次union()操作,但这些成本相比ElogE的增长数量级可忽略不计(详见1.5)
空间:E
public class KruskalMST {
private Queue<Edge> mst;
public KruskalMST(EdgeWeightedGraph G) {
mst = new Queue<Edge>();
MinPQ<Edge> pq = new MinPQ<Edge>(G.edges());
UF uf = new UF(G.V()); //Reference: Union Find in Chapter 1
while (!pq.isEmpty() && mst.size() < G.V() - 1) {
Edge e = pq.delMin(); // Get min weight edge on pq
int v = e.either(), w = e.other(v); // and its vertices.
if (uf.connected(v, w))
continue; // Ignore ineligible edges.
uf.union(v, w); // Merge components.
mst.enqueue(e); // Add edge to mst.
}
}
public Iterable<Edge> edges(){
return mst;
}
public double weight() // See Exercise 4.3.31.
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。