一,介紹
本文實現帶權圖的最短路徑算法。給定圖中一個頂點,求解該頂點到圖中所有其他頂點的最短路徑 以及 最短路徑的長度。在決定寫這篇文章之前,在網上找了很多關於Dijkstra算法實現,但大部分是不帶權的。不帶權的Dijkstra算法要簡單得多(可參考我的另一篇:無向圖的最短路徑算法JAVA實現);而對於帶權的Dijkstra算法,最關鍵的是如何“更新鄰接點的權值”。本文采用最小堆作為輔助,以重新構造堆的方式實現更新鄰接點權值。
對於圖而言,存在有向圖和無向圖。本算法只需要修改一行代碼,即可同時實現帶權有向圖的Dijkstra和帶權無向圖的Dijkstra。因為,不管圖是否是有向的還是無向的,只是構造圖的方式不一樣而已,而 Dijkstra算法都是一樣的。
Dijkstra算法的實現需要一個輔助堆,用來選取當前到源點的距離 最小的那個頂點,這里采用了最小堆來實現。用最小堆保存圖中所有頂點到源點的距離,因為Dijkstra算法運行過程中,需要每次選取當前到源點 距離最短 的那個頂點,這步操作用“出堆”很容易實現,但是,當選出該頂點之后, 需要不斷地更新該頂點的鄰接點到源點的距離。而最小堆不能很好地支持這種更新操作(關於最小堆可參考:),這也是為什么《算法導論》中推薦使用菲波拉契堆或者配對堆實現Dijkstra算法的原因。
二,Dijkstra實現思路
①初始化,源點的距離初始化為0(源點到它自己的距離當然是0了),源點的前驅頂點為null(因為是從源點開始的嘛,求源點到圖中所有其他頂點的minDistance...)。所有其他頂點的前驅頂點也初始化為null,且頂點的“距離”(dist)屬性初始化為無窮大(Integer.MAX_VALUE),即其他頂點到源點的距離 為無窮大。
②構造堆。將所有的頂點按照“距離”屬性(dist) 構造最小堆。顯然,由於源點的“距離”屬性為0,其他頂點的“距離”屬性為Integer.MAX_VALUE,故最開始構造的堆的 堆頂元素為源點。
③只要堆中還存在元素(while循環),執行deleteMin從堆中刪除堆頂元素,記該元素為v,尋找v的所有鄰接點,更新v的所有鄰接點的距離。怎么更新的呢?就是比較:❶v的鄰接點到源點的距離(dist屬性) ; ❷v到源點的距離(dist屬性) 加上 v 到v的鄰接點的這條 邊的權值
v的鄰接點的距離(dist屬性)取 ❶ ❷ 中較小的那個。
偽代碼如下:
DIJKSTRA(G,w,s)
初始化
構造堆(Q=V(G))
while(!isEmpty(Q))
v=EXTRACT-MIN(Q)
foreach vertex v_adj belogns to Adj[v]
更新v的鄰接點 v_adj
三,具體代碼實現
在講解具體實現前,先介紹下如何構造圖。假設圖中的數據存儲在文件中,文件的格式如下:
(圖的頂點及邊信息---暫且用無向圖舉例)
第一列代表頂點的編號(不用管) ;第二列表示 邊的 起始頂點的標識(vertexLabel)
第三列表示 邊的 終點的標識;第四列表示邊的權值。比如,對於權值為1的那條邊而言,它對應的 起始頂點編號為0,對應的結點頂點的編號為1
關於圖的解釋,可參考:
這里由於是帶權圖,故邊類(Edge.java)需要有一個權值(邊的權值)。
private class Edge{
private int weight;//邊的權值(帶權圖)
private Vertex endVertex;
public Edge(int weight, Vertex endVertex) {
this.weight = weight;
this.endVertex = endVertex;
}
圖采用的是鄰接表實現,因此每個頂點都會有一個鄰接點列表。
1 private class Vertex implements Comparable<Vertex> 2 { 3 private String vertexLabel;//頂點標識 4 private List<Edge> adjEdges;//頂點的所有鄰接邊(點) 5 private int dist;//頂點到源點的最短距離 6 private Vertex preNode;//追溯最短路徑 7 8 public Vertex(String vertexLabel){ 9 this.vertexLabel = vertexLabel; 10 adjEdges = new LinkedList<Edge>(); 11 dist = Integer.MAX_VALUE; 12 preNode = null; 13 } 14 15 @Override 16 public int compareTo(Vertex v) { 17 if(this.dist > v.dist) 18 return 1; 19 else if(this.dist < v.dist) 20 return -1; 21 return 0; 22 } 23 }
①第4行 adjEdges 是頂點的鄰接點列表,表明圖采用的是鄰接表存儲。第5行 dist 表示的是該頂點到源點的最短距離(從而不需要一個單獨的距離數組)。第6行preNode 表示該頂點的前驅頂點, 用來記錄源點到該頂點路徑中經歷了哪些頂點。
②Vertex類實現了Comparable接口,因為需要將頂點存儲到最小堆中,而最小堆存儲的元素需要實現Comparable接口(可以進行頂點的比較)。
最關鍵的是實現Dijkstra算法中用到的最小堆。關於最小堆的實現,可參考:數據結構--堆的實現之深入分析 本程序就是用的它。
然后是 dijkstra 的具體實現代碼:
1 public void dijkstra(){ 2 BinaryHeap<Vertex> heap = new BinaryHeap<WeightedGraph.Vertex>(); 3 init(heap);//inital heap 4 5 while(!heap.isEmpty()) 6 { 7 Vertex v = heap.deleteMin(); 8 List<Edge> adjEdges = v.adjEdges;//獲取v的所有鄰接點 9 for (Edge e : adjEdges) { 10 Vertex adjNode = e.endVertex; 11 //update 12 if(adjNode.dist > e.weight + v.dist){ 13 adjNode.dist = e.weight + v.dist; 14 adjNode.preNode = v; 15 } 16 }//end for 17 18 //更新之后破壞了堆序性質,需要進行堆調整,這里直接重新構造堆(相當於decreaseKey) 19 heap.buildHeap(); 20 } 21 22 }
①第7行,從堆中出一個距離源點路徑最短的頂點。剛好符合堆的基本操作(刪除堆頂元素),這里也體現了Dijkstra是個貪心算法。
②第8-10行,獲取頂點的鄰接點
③第12行--15行的if語句,執行更新操作。關於更新操作的具體解釋,可參考上面的介紹。
④由於 ③中的更新操作,破壞了堆序的性質,故需要進行堆調整。但是如何調整呢?由於堆不支持將堆中某個結點的權值降低,故在第19行,直接再次建堆。以保證堆序性質 。但是這里的時間復雜度就大了,故推薦使用更好的數據結構來實現,如Fib堆,因為Fib堆的將某個結點的權值降低是很方便的。
時間復雜度簡要分析如下:buildHeap()的時間復雜度為O(N),對於圖中每個頂點v,出堆時都需要重新構造堆,故最壞情況下時間復雜度為O(V^2)
整個完整代碼實現如下:
import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; public class WeightedGraph{ private class Vertex implements Comparable<Vertex> { private String vertexLabel;//頂點標識 private List<Edge> adjEdges;//頂點的所有鄰接邊(點) private int dist;//頂點到源點的最短距離 private Vertex preNode;//前驅頂點 public Vertex(String vertexLabel){ this.vertexLabel = vertexLabel; adjEdges = new LinkedList<Edge>(); dist = Integer.MAX_VALUE; preNode = null; } @Override public int compareTo(Vertex v) { if(this.dist > v.dist) return 1; else if(this.dist < v.dist) return -1; return 0; } } private class Edge{ private int weight;//邊的權值(帶權圖) private Vertex endVertex; public Edge(int weight, Vertex endVertex) { this.weight = weight; this.endVertex = endVertex; } } private Map<String, Vertex> weightedGraph;//存儲圖(各個頂點) private Vertex startVertex;//單源最短路徑的起始頂點 //圖的信息保存在文件中,從文件中讀取成字符串graphContent public WeightedGraph(String graphContent) { weightedGraph = new LinkedHashMap<String, WeightedGraph.Vertex>(); buildGraph(graphContent);//解析字符串構造圖 } private void buildGraph(String graphContent){ String[] lines = graphContent.split("\n"); String startNodeLabel, endNodeLabel; Vertex startNode, endNode; int weight; for(int i = 0; i < lines.length; i++){ String[] nodesInfo = lines[i].split(","); startNodeLabel = nodesInfo[1]; endNodeLabel = nodesInfo[2]; weight = Integer.valueOf(nodesInfo[3]); endNode = weightedGraph.get(endNodeLabel); if(endNode == null){ endNode = new Vertex(endNodeLabel); weightedGraph.put(endNodeLabel, endNode); } startNode = weightedGraph.get(startNodeLabel); if(startNode == null){ startNode = new Vertex(startNodeLabel); weightedGraph.put(startNodeLabel, startNode); } Edge e = new Edge(weight, endNode); //對於無向圖而言,起點和終點都要添加邊 // endNode.adjEdges.add(e); startNode.adjEdges.add(e); } startVertex = weightedGraph.get(lines[0].split(",")[1]);//總是以文件中第一行第二列的那個標識頂點作為源點 } public void dijkstra(){ BinaryHeap<Vertex> heap = new BinaryHeap<WeightedGraph.Vertex>(); init(heap);//inital heap while(!heap.isEmpty()) { Vertex v = heap.deleteMin(); List<Edge> adjEdges = v.adjEdges;//獲取v的所有鄰接點 for (Edge e : adjEdges) { Vertex adjNode = e.endVertex; //update if(adjNode.dist > e.weight + v.dist){ adjNode.dist = e.weight + v.dist; adjNode.preNode = v; } }//end for //更新之后破壞了堆序性質,需要進行堆調整,這里直接重新構造堆(相當於decreaseKey) heap.buildHeap(); } } private void init(BinaryHeap<Vertex> heap){ startVertex.dist = 0;//源點到其自身的距離為0 for (Vertex v : weightedGraph.values()) { heap.insert(v); } } public void showDistance(){ for (Vertex v : weightedGraph.values()) { printPath(v); System.out.println(); System.out.println("頂點 " + v.vertexLabel + "到源點" + startVertex.vertexLabel + " 的距離: " + v.dist); } } //打印源點到 end 頂點的 最短路徑 private void printPath(Vertex end) { if(end.preNode != null) printPath(end.preNode); System.out.print(end.vertexLabel + "--> "); } }
buildGraph()方法中:如果是有向圖,只需要起點添加邊;如果是無向圖,則起點和終點都需要添加邊。但不管是有向圖還是無向圖Dijkstra算法都一樣。
Edge e = new Edge(weight, endNode); //對於無向圖而言,起點和終點都要添加邊 // endNode.adjEdges.add(e); startNode.adjEdges.add(e);
關於如何測試WeightedGraph.java,需要構造一個圖。構造圖:可參考有向圖的拓撲排序算法JAVA實現 中的“完整代碼實現”中的FileUtil.java 和 TestXXX.java
public class TestDijkstra { public static void main(String[] args) { String graphFilePath; if(args.length == 0) graphFilePath = "F:\\graph2.txt"; else graphFilePath = args[0]; String graphContent = FileUtil.read(graphFilePath, null); WeightedGraph graph = new WeightedGraph(graphContent); graph.dijkstra(); graph.showDistance(); } }