帶權圖的最短路徑算法(Dijkstra)實現


一,介紹

本文實現帶權圖的最短路徑算法。給定圖中一個頂點,求解該頂點到圖中所有其他頂點的最短路徑 以及 最短路徑的長度。在決定寫這篇文章之前,在網上找了很多關於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();
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM