一、 加權無向圖概述
加權無向圖是在無向圖的基礎上,為每條無向邊關聯一個成本或是權重值。
在導航中,我們常常需要判斷圖中由若干邊組成的路徑是否是長度最短,時間最短或是通行成本最低,權重不一定表示距離,可以多樣化的表示為跟成本相關的數據。
二、 加權無向圖實現
由於無向圖的邊關聯了權重,因此需要把邊作為一個對象處理,包含兩個頂點和邊的權重三個重要屬性,具體實現如下
/** * 加權無向圖的邊對象 * @author jiyukai */ public class Edge implements Comparable<Edge>{
//邊的頂點1 private int v;
//邊的頂點2 private int w;
//邊的權重 private int weight;
public Edge(int v, int w, int weight) { super(); this.v = v; this.w = w; this.weight = weight; }
/** * 獲取邊的權重 * @return */ public int getWeight() { return weight; }
/** * 獲取頂點 * @return */ public int getV() { return v; }
/** * 獲取v頂點外的另一個頂點 * @return */ public int getEither(int k) { //頂點與v相等,則返回另一個 if(k==v) { return w; }else { return v; } }
/** * 比較其他邊和當前邊的權重大小 */ @Override public int compareTo(Edge o) {
int cmp;
if(this.getWeight() > o.getWeight()) { cmp=1; }else if(this.getWeight() < o.getWeight()) { cmp=-1; }else { cmp=0; }
return cmp; }
} |
三、 最小生成樹定義
最小生成樹用於在加權無向圖中,找到成本最低的路徑組成的連通子圖,比如從平安金融中心出發到華潤大廈,存在多條路徑可達的情況下,如何找到路徑最短的一條。
如下圖,邊的權重表示距離,從頂點0出發到達頂點4,紅色邊描繪的即是一棵最小生成樹,它表示存在多條可到達頂點4路徑的情況下,紅色邊集合的權重相加是最小的。
最小生成樹定義:
圖的生成樹是一棵含有所有頂點的無環連通子圖,圖的最小生成樹是包含所有頂點的子圖中,所有邊相加權重和最小的無環連通子圖。
約定:因最小生成樹需要包含圖的所有頂點,因此只考慮連通圖。
最小生成樹特性:
特性1:連接最小生成樹中任意兩個頂點會形成一個環
特性2:從樹中刪除任意一條邊,會把最小生成樹切割成兩棵獨立的子樹
最小生成樹的切割定理:
要找出一幅圖中的最小生成樹,需要通過圖的切割定理完成
切割:
將一幅圖按一定規則分割成兩顆非空且沒有交集的集合。
橫切邊:
連接兩個屬於不同集合的頂點的邊稱之為橫切邊
將上圖中的最小生成樹的2-3相連的邊斷開,則分成了如下兩個(藍色和黃色)非空無交集的集合,此時連接兩個集合的2-3,2-4,1-4都是橫切邊。
切分定理:在一副加權圖中,給定任意的切分,它的橫切邊中的權重最小者必然屬於圖中的最小生成樹
四、 貪心思想
貪心思想是實現最小生成樹的基礎思想,它的原理是通過切分定理,從一個頂點出發,使用切分定理找到最小生成樹的一條邊,並將改邊連接的另一個頂點和邊一起加到最小生成樹中
再通過切分定理依次往下找到組成最小生成樹的邊,最終遍歷完所有的N個頂點,組成了N-1條邊的的最小生成樹。
五、 Prim算法原理
Prim算法就是利用貪心思想實現的最小生成樹,它主要把最小生成樹中的頂點看成一個集合,把其他頂點看成另一個集合,每次尋找連通兩個集合間的若干橫切邊
找到最小的橫切邊並加到最小生成樹中,下面我們通過圖示過程來了解Prim
步驟1:如上無向圖中,頂點0作為起點,最先加入到最小生成樹中,此時頂點0為一個集合(藍色表示),頂點1,2,3,4為另一個集合(黃色表示),連通兩個集合的橫切邊為0-1和0-2,
通過比較0-1的權重較小,因此取0-1這條邊和頂點1加入到最小生成樹中
步驟2:如上無向圖中,0和1作為一個最小生成樹的集合,與另外的頂點2,3,4組成的集合之間的橫切邊是1-4,1-2,0-2,此時發現1-2的權重最小,添加到最小生成樹中
此時與頂點3,4組成的集合的橫切邊是1-4,2-4,2-3,此時發現2-3權重最小,於是將頂點3和邊2-3加入到最小生成樹中,直到最后遍歷完所有頂點,連通的子圖即是最小生成樹。
六、 Prim實現
實現Prim算法的代碼前,我們再通過一次帶真實權重的無向圖演示來還原整個過程,我們需要聲明一個最小索引優先隊列,索引為頂點,值用於存放頂點與非最小生成樹頂點的橫切邊
存在多條時取最小即可。
步驟一:頂點0為起點,兩條橫切邊的權重如圖,將索引優先隊列頂點1和2的值更新為權重,此時取到更小的權重0.04,並從索引優先隊列中彈出最小值對應的索引,即頂點1加入到最小生成樹中
步驟二:0和1組成最小生成樹后,更新索引優先隊列,此時頂點0和1都指向頂點2,則取值更小的0.15,從橫切邊中取到最小的0.15權重的邊1-2,再從索引優先隊列中彈出最小值0.15對應的索引2
加入到最小生成樹中
如下圖演示依次類推,最后得到最小生成樹
import com.data.struct.common.list.queue.Queue; import com.data.struct.common.tree.priority.queue.IndexMinPriorityQueue;
/** * prim算法求最小生成樹 * @author jiyukai */ public class PrimMST {
// 索引存放頂點,值為頂點與最小生成樹的最短橫切邊的權重 private Double[] edgeWeight;
// 索引存放頂點,值為頂點與最小生成樹的最短橫切邊 private Edge[] edges;
// 存放標記頂點的數組,記錄當前頂點是否在最小生成樹中 private boolean[] flags;
// 存放最小生成樹頂點與非最小生成樹頂點的目標橫切邊,索引為頂點 private IndexMinPriorityQueue<Double> minQueue;
/** * 最小生成樹 * @param G * @param s * @throws Exception */ public PrimMST(EdgeGraph G) throws Exception { // 索引代表頂點,初始狀態下先將頂點與最小生成樹的橫切邊設置為無窮大,並將初始頂點橫切邊設置為0.0 edgeWeight = new Double[G.V()]; for (int i = 0; i < G.V(); i++) { edgeWeight[i] = Double.POSITIVE_INFINITY; } edgeWeight[0] = 0.0;
edges = new Edge[G.V()]; flags = new boolean[G.V()];
// 使用最小優先隊列,是為了方便從從隊列中取出非最小生成樹頂點到最小生成樹的最短橫切邊 minQueue = new IndexMinPriorityQueue<Double>(G.V()+1); minQueue.insert(0, 0.0);
// 隊列不為空時遍歷所有頂點 while (!minQueue.isEmpty()) { visit(G, minQueue.delMin()); } }
/** * 訪問頂點,逐步生成最小生成樹 * @param G * @param v * @throws Exception */ private void visit(EdgeGraph G, int v) throws Exception { // 把頂點v添加到最小生成樹中 flags[v] = true;
// 訪問頂點的鄰接邊,獲取每一條邊 for (Edge e : G.qTable[v]) { // 訪問邊連接的另一個頂點w,看是否在樹中,在樹中跳過,不在樹中,則需要比較edgeWeight[w]和v-w的大小,保留最小值 int w = e.getEither(v);
if (flags[w]) { continue; }
if (e.getWeight() < edgeWeight[w]) {
edgeWeight[w] = Double.valueOf(e.getWeight());
edges[w] = e;
if (minQueue.contains(w)) { minQueue.changeItem(w, Double.valueOf(e.getWeight())); } else { minQueue.insert(w, Double.valueOf(e.getWeight())); } } } }
/** * 獲取最小生成樹的所有邊 * @return */ private Queue<Edge> getAllEdge() { Queue<Edge> edgeQueue = new Queue<>(); // 遍歷edgeTo數組,找到每一條邊,添加到隊列中 for (int i = 0; i < flags.length; i++) { if (edges[i] != null) { edgeQueue.enqueue(edges[i]); } } return edgeQueue; } } |
七、 Kruskal算法原理
Kruskal算法是計算一副加權無向圖的最小生成樹的另外一種算法,它的主要思想是按照邊的權重,從小到大進行排序,從最小值開始遍歷,將最小邊和邊的兩個頂點加入最小生成樹中
已加入的邊和頂點不會再加入最小生成樹的邊構成環,直到樹中含有V-1條邊為止。
步驟一:如下的無向連通圖,對邊和權重按從小到大排序后,首先取最小邊的3-4組成一顆最小生成樹
步驟二:繼續遍歷把0-1加入到另一顆最小生成樹中
步驟三:繼續遍歷把1-2加入到0-1組成的最小生成樹中
步驟四:繼續遍歷把2-3加入到0-1,1-2組成的最小生成樹中,此時無需再往后遍歷0-2,2-4,1-4,因為頂點都已在最小生成樹中,樹中含有V-1條邊,則Kruskal計算結束,獲取到一顆最小生成樹
八、 Kruskal實現
/** * kruskal算法求最小生成樹 * @author jiyukai */ public class KruskalMST {
//索引存放頂點,值為頂點與最小生成樹的最短橫切邊 private Queue<Edge> edges;
//聲明並查集,用來合並兩個頂點到一棵生成樹中,和判斷兩個頂點是否在一棵樹中 private UF_Shorter_Tree usf;
//存儲圖中所有的邊,使用最小優先隊列排序,方便彈出最小值 private MinPriorityQueue<Edge> minQueue;
public KruskalMST(EdgeGraph G) { edges = new Queue<Edge>(); usf = new UF_Shorter_Tree(G.V());
//初始化最小優先隊列並插入所有邊,使邊有序 minQueue = new MinPriorityQueue<Edge>(G.E()+1); for(Edge e : G.edges()) { minQueue.insert(e); }
while (!minQueue.isEmpty() && edges.size() < (G.V() - 1)) { //獲取最小優先隊列中的最小邊 Edge minEdge = minQueue.delMin();
//獲取邊的兩個頂點 int v = minEdge.getV(); int w = minEdge.getEither(v);
//如果v和w在一棵樹中,則不再合並,如果不在一棵樹中,則合並進最小生成樹,並把邊添加到最小生成樹的邊中 if(usf.connected(v, w)) { continue; }else { usf.union(v, w); edges.enqueue(minEdge); } } } } |