加權圖是一種爲每條邊關聯一個權值的圖模型,這種圖能夠表示許多應用,好比在一副航空圖中,邊表示航線,權值就能夠表示距離或費用;在一副電路圖中,邊表示導線,權值就能夠表示導線的長度或成本。在這些情形中,最使人感興趣的即是如何將成本最小化。最小生成樹就是用於在加權無向圖中解決這類問題的。最小生成樹相關的算法在通訊、電子、水利、網絡、交通燈行業具備普遍的應用。java
圖的生成樹是它的一顆含有其全部頂點的無環連通子圖,一副加權無向圖的最小生成樹(Minimum spanning tree)是它的一顆權值(樹中全部邊的權值之和)最小的生成樹。算法
圖的一種切分是將圖的全部頂點分爲兩個非空且不重複的集合。橫切邊是一條鏈接兩個屬於不一樣集合頂點的邊。
一般經過指定一個頂點集並隱式地認爲它的補集爲另外一個頂點集來指定一個切分。這樣,一條橫切邊就是鏈接該集合的一個頂點和不在該集合中的另外一個頂點的一條邊。數組
切分定理的內容爲:在一副加權圖中,給定任意的切分,它的橫切邊中的權重最小者必然屬於圖的最小生成樹。
切分定理是最小生成樹算法的理論依據。
要證實切分定理,須要知道樹的兩個重要性質:網絡
切分定理是全部解決最小生成樹問題算法的基礎,這些算法都是一種貪心算法的特殊狀況,貪心算法是一類在每一步選擇中都採起在當前狀態下最好或最優的選擇,從而但願致使結果是最好或最優的算法。解決最小生成樹問題時,會使用切分定理找到最小生成樹的一條邊,不斷重複直到找到最小生成樹的全部邊。這些算法之間的區別之處在於保存切分和斷定權重最小的橫切邊的方式。數據結構
最小生成樹的貪心算法:一副加權無向圖中,在初始狀態下全部邊均爲灰色,找到一種切分,它產生的橫切邊均不爲黑色,將它權重最小的橫切邊標記爲黑色,如此反覆,直到標記了V-1條黑色邊爲止。app
其中V爲圖中頂點的數量,那麼要將這些頂點所有鏈接,至少須要V-1條邊。根據切分定理,全部被標記爲黑色的邊都屬於最小生成樹,若是黑色邊的數量小於V-1,那麼必然還存在不會產生黑色邊的切分,只要找夠V-1條黑色邊,最小生成樹就完成了。學習
加權無向圖的數據結構沒有沿用以前無向圖的數據結構,而是從新定義了Edge和EdgeWeightedGraph類,分別用於表示帶權重的邊和加權無向圖。ui
public class Edge implements Comparable<Edge> { private final int v; private final int 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 this.weight; } public int either() { return this.v; } public int other(int vertex) { if (v == vertex) return w; if (w == vertex) 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); } }
either和other方法能夠返回邊鏈接的兩個端點,weight表示邊的權重。this
public class EdgeWeightedGraph { private static final String NEWLINE = System.getProperty("line.separator"); private final int V; // vertex private int E; // edge 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<Edge>(); } } public EdgeWeightedGraph(In in) { this(in.readInt()); int E = in.readInt(); for (int i = 0; i < E; i++) { int v = in.readInt(); int w = in.readInt(); double weight = in.readDouble(); Edge e = new Edge(v, w, weight); addEdge(e); } } 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 String toString() { StringBuilder s = new StringBuilder(); s.append(V + " vertices, " + E + " edges " + NEWLINE); for (int v = 0; v < V; v++) { s.append(v + ": "); for (Edge w : adj[v]) { s.append(w + " | "); } s.append(NEWLINE); } return s.toString(); } public Bag<Edge> edges() { Bag<Edge> b = new Bag<Edge>(); for (int v = 0; v < V; v++) { for (Edge w : adj[v]) { b.add(w); } } return b; }
EdgeWeightedGraph與無向圖中的Graph很是相似,只是用Edge對象替代了Graph中的整數來做爲鏈表的結點。adj(int v)方法能夠根據頂點而索引到對應的鄰接表,每條邊都會出現兩次,若是一條邊鏈接了頂點v和w,那麼這條邊會同時被添加到v和w對應的領接表中。spa
將要學習的第一種計算最小生成樹的方法叫作Prim算法,它的每一部都會爲一顆生長中的樹添加一條邊。一開始這棵樹只有一個頂點,而後會向它添加V-1條邊,每次老是將下一條鏈接樹的頂點與不在樹中的頂點且權重最小的邊加入樹中。
但如何才能高效地找到權重最小的邊呢,使用優先隊列即可以達到這個目的,而且保證足夠高的效率。由於要尋找的是權重最小的邊,因此這裏將使用查找最小元素的優先隊列MinPQ。
此外,Prim算法還會使用一個由頂點索引的boolean數組marked[],和一條名爲mst的隊列,前者用來指示已經加入到最小生成樹中的頂點,隊列則用來保存包含在最小生成樹中的邊。
每當在向樹中添加了一條邊時,也向樹中添加了一個頂點。要維護一個包含全部橫切邊的集合,就要將鏈接這個頂點和其餘全部不在樹中的頂點的邊加入優先隊列,經過marked[]數組能夠識別這樣的邊。須要注意的是,隨着橫切邊的不斷加入,以前加入的邊中,那些鏈接新加入樹中的頂點與其餘已經在樹中頂點的全部邊都失效了,由於這樣的邊都已經不是橫切邊了,它的兩個頂點都在樹中,這樣的邊是不會被加入到mst隊列中的。
接下來用tinyEWG.txt的數據來直觀地觀察算法的軌跡,tinyEWG.txt的內容以下:
8 16 4 5 0.35 4 7 0.37 5 7 0.28 0 7 0.16 1 5 0.32 0 4 0.38 2 3 0.17 1 7 0.19 0 2 0.26 1 2 0.36 1 3 0.29 2 7 0.34 6 2 0.40 3 6 0.52 6 0 0.58 6 4 0.93
它表示的圖包含8個頂點,16條邊,末尾的double數值表示邊的權重。
下圖是算法在處理tinyEWG.txt時的軌跡,每一張圖都是算法訪問過一個頂點以後(被添加到樹中,鄰接鏈表中的邊也已經被處理完成),圖和優先隊列的狀態。優先隊列的內容被按照順序顯示在一側,樹中的新頂點旁邊有個星號。
算法構造最小生成樹的過程爲:
算法的具體實現:
public class LazyPrimMST { private boolean[] marked; private Queue<Edge> mst; private MinPQ<Edge> pq; public LazyPrimMST(EdgeWeightedGraph G) { pq = new MinPQ<Edge>(); marked = new boolean[G.V()]; mst = new Queue<Edge>(); 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); } } public 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; } // cmd /c --% java algs4.four.LazyPrimMST ..\..\..\algs4-data\tinyEWG.txt public static void main(String[] args) { In in = new In(args[0]); EdgeWeightedGraph ewg = new EdgeWeightedGraph(in); LazyPrimMST lazyPrim = new LazyPrimMST(ewg); double weight=0; for (Edge e : lazyPrim.edges()) { weight += e.weight(); StdOut.println(e); } StdOut.println(weight); } }
visit()方法的做用是爲樹添加一個頂點,將它標記爲「已訪問」,並將與它關聯的全部未失效的邊加入優先隊列中。在while循環中,會從優先隊列取出一條邊,若是它沒有失效,就把它添加到樹中,不然只是將其從優先隊列刪除。而後再根據添加到樹中的邊的頂點,更新優先隊列中橫切邊的集合。
Prim算法是一條邊一條邊地來構造最小生成樹,每一步都爲一棵樹添加一條邊。接下來要學習的Kruskal算法處理問題的方式則是按照邊的權重順序,從小到大將邊添加到最小生成樹中,加入的邊不會與已經加入的邊構成環,直到樹中含有V-1條邊爲止。從一片由V顆單結點的樹構成的森林開始,不斷將兩棵樹合併,直到只剩下一顆樹,它就是最小生成樹。
一樣是處理tinyEWG.txt,Kruskal算法的軌跡以下圖:
【】
該算法首先會將全部的邊加入到優先隊列並按權重順序排列,而後依次從優先隊列拿到最小的邊加入到最小生成樹中,而後輪處處理1-三、1-五、2-7這三條邊時,發現它們會使最小生成樹造成環,說明這些頂點已經被包含到了最小生成樹中,屬於失效的邊;接着繼續處理4-5,隨後1-二、4-七、0-4又被丟棄,把6-2加入樹中後,最小生成樹已經有了V-1條邊,最小生成樹已經造成,查找結束。
算法的具體實現爲:
public class KruskalMST { private Queue<Edge> mst; private double _weight = 0; public KruskalMST(EdgeWeightedGraph G) { mst = new Queue<Edge>(); MinPQ<Edge> pq = new MinPQ<Edge>(); UF uf = new UF(G.V()); for (Edge e : G.edges()) { pq.insert(e); } 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); _weight += e.weight(); } } public Iterable<Edge> edges() { return mst; } public double weight() { return _weight; } // cmd /c --% java algs4.four.KruskalMST ..\..\..\algs4-data\tinyEWG.txt public static void main(String[] args) { In in = new In(args[0]); EdgeWeightedGraph ewg = new EdgeWeightedGraph(in); KruskalMST kruskalMST = new KruskalMST(ewg); for (Edge e : kruskalMST.edges()) { StdOut.println(e); } StdOut.println(kruskalMST.weight()); } }
這裏一樣使用了MinPQ來爲邊排序,並使用了以前Union-Find算法中實現的的Quick Union數據結構,用它能夠方便地識別會造成環的邊,最終生成的最小生成樹一樣保存在名爲mst的隊列中。