1. 最小生成樹的定義
生成樹指的是含有所有頂點的無環連通子圖。注意這其中的三個限定條件:
1)包含了所有的頂點
2)不存在環
3)連通圖
如上圖所示。就是一個生成樹。
而最小生成樹指的是所有的邊的權值加起來最小的生成樹。最小生成樹的重要應用領域太多,包括各種網絡問題,例如電力分配網絡,航空、鐵路規划等問題。
2. 加權無向圖的數據類型
//帶權重的邊的數據類型 public class Edge implements Comparable<Edge> { int v; int w; 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) //返回另一個頂點 { if(vertex == this.v) return this.w; else if(vertex == this.w) return this.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() 目前比較難以理解,但之后會有比較大的用處。
//加權無向圖 public class EdgeWeightedGraph { private int V; private int E; private Bag<Edge>[] adj; //注意這里的鄰接矩陣不再是鄰接的點了,而是鄰接的邊的集合 public EdgeWeightedGraph(int V) { this.V = V; 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(); int 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; } }
3. 尋找最小生成樹
首先要了解,假設所有的邊的權重不相同的情況下,每幅連通圖都只有唯一的一顆最小生成樹。而且最小生成樹有一個很重要的切分定理。
切分定理:在一副加權圖中,給定任意的切分,它的橫切邊中權重最小者必然屬於最小生成樹。也就是說假如把所有頂點任意的分成兩堆,那么能夠把兩堆頂點中連起來的權重最小的邊肯定屬於最小生成樹。
從上圖中可以看出,在兩堆頂點中,能夠把7和0連起來的邊的權重(在這里可以看成是距離)是最小的,所以這條邊肯定屬於最小生成樹。
要把整顆生成樹找出來,可以用貪心算法,一條邊一條邊找出來加到生成樹中。
4. prim算法
首先需要厘清prim算法中用到的數據結構。
1)樹中頂點的集合marked[],假如marked[v] = true,則說明,v此時已經在樹中。
2)樹中的邊。一條隊列mst來存儲當前樹中的所有邊。
3)橫切邊。一條優先隊列MinPQ<Edge>來存儲當前考慮的可能在最小生成樹中的候選邊。
prim也是貪心算法的一種。具體過程如下:
1)先從任意一個頂點開始,把該頂點的所有鄰接邊加入優先隊列MinPQ中,並把該頂點加入最小生成樹中的marked數組。
2)在優先隊列MinPQ中權重最小的那條邊,把這條邊加入最小生成樹mst中,並把邊的另一個頂點加入marked數組,並把新加入的頂點的鄰接邊也加入到優先隊列MinPQ中。
3)繼續在優先隊列MinPQ中找到權重最小的邊,並把該條邊加入mst中,並繼續把邊的另一個頂點計入marker數組,並把新加入的頂點的鄰接邊也加入到優先隊列MinPQ中。
4)...不斷重復當前過程。
所以prim算法的效果就相當於從某一點出發,不斷在當前的樹的基礎上延伸新的邊,最后形成一顆完整的樹。
public class LazyPrimMST { private boolean[] marked; private Queue<Edge> mst; //隊列 private MinPQ<Edge> pq; //優先隊列 public LazyPrimMST(EdgeWeightedGraph G) { marked = new boolean[G.V()]; mst = new Queue<Edge>(); pq = new MinPQ<Edge>(); visit(G, 0); while(!pq.isEmpty()) //注意循環條件,等到最小生成樹完全生成后,最后MinPQ中會一條一條被刪掉,直到為空 { 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 class PrimMST { private boolean[] marked; private Edge[] edgeTo; private double[] distTo; private IndexMinPQ<Double> pq; public PrimMST(EdgeWeightedGraph G) { marked = new boolean[G.V()]; edgeTo = new Edge[G.V()]; distTo = new Double[G.V()]; pq = new IndexMinPQ<>(G.V()); //新建一個容量大小為索引優先隊列 for(int v= 0;v<G.V(); v++) distTo[v] = Double.POSITIVE_INFINITY; pq.insert(0, 0.0); distTo[0] = 0.0; while(!pq.isEmpty()) visit(G, pq.delMin()); } public 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]) { distTo[w] = e.weight(); edgeTo[w] = e; if(pq.contains(w)) pq.change(w, distTo[w]); else pq.insert(w, distTo[w]); } } } }
5. kruskal算法
於prim算法是在一顆小樹的基礎上,一條邊一條邊延展從而生成一顆大樹不同,kruskal算法則是全面開花,最后連成一棵樹。
kruskal的算法思想也很簡單:按照權重將邊排序依次添加進樹中,如果邊會形成環,則棄之,否則,加入樹中。
public class kruskalMST { private Queue<Edge> mst; public kruskalMST(EdgeWeightedGraph G) { mst = new Queue<Edge>(); MinPQ<Edge> pq = new MinPQ<Edge>(); UF uf = new UF(); 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); //添加到樹中 } } public Iterable<Edge> edges() { return mst;} }
參考資料:《算法》第四版