算法-11| 最短路徑| Dijkstra算法

有向有權圖

圖的兩種搜索算法,深度優先搜索和廣度優先搜索。這兩種算法主要是針對無權圖的搜索算法。

針對有權圖,也就是圖中的每條邊都有一個權重,該如何計算兩點之間的最短路徑(經過的邊的權重和最小)呢?常用的最短路徑算法(Shortest Path Algorithm)。

地圖軟件的最優路線是如何計算出來?底層依賴什麼算法?這裏的最優,比如最短路線、最少用時路線、最少紅綠燈路線等等。

最短路線:解決軟件開發中的實際問題最重要的一點就是建模,也就是將複雜的場景抽象成具體的數據結構。

把地圖抽象成圖,把每個岔路口看作一個頂點,岔路口與岔路口之間的路看作一條邊,路的長度就是邊的權重。 如果路是單行道,我們就在兩個頂點之間畫一條有向邊;如果路是雙行道,我們就在兩個頂點之 間畫兩條方向不同的邊。這樣,整個地圖就被抽象成一個有向有

權圖。於是,要求解的問題就轉化爲,在一個有向有權圖中,求兩個頂點間的最短路徑。

import java.util.LinkedList;
// 有向有權圖的鄰接表表示
public class Graph {
    private LinkedList<Edge> adj[]; // 鄰接表
    private int v; // 頂點個數
    public Graph(int v) {
        this.v = v;
        this.adj = new LinkedList[v];
        for (int i = 0; i < v; ++i) {
            this.adj[i] = new LinkedList<>();
        }
    }
    public void addEdge(int s, int t, int w) { // 添加一條邊
        this.adj[s].add(new Edge(s, t, w));
    }
    private class Edge {
        public int sid; // 邊的起始頂點編號
        public int tid; // 邊的終止頂點編號
        public int w; // 權重
        public Edge(int sid, int tid, int w) {
            this.sid = sid;
            this.tid = tid;
            this.w = w;
        }
    }
    // 下面這個類是爲了 dijkstra 實現用的
    private class Vertex implements Comparable<Vertex> {
        public int id; // 頂點編號 ID
        public int dist; // 從起始頂點到這個頂點的距離
        public Vertex(int id, int dist) {
            this.id = id;
            this.dist = dist;
        }
        @Override
        public int compareTo(Vertex o) { // 按照 dist 從小到大排序
            if (o.dist > this.dist) return -1;
            else return 1;
        }
    }
}

Dijkstra 算法

解決這個問題,有一個非常經典的算法,最短路徑算法,單源最短路徑算 法(一個頂點到一個頂點),最出名的莫過於 Dijkstra 算法。Dijkstra 算法的工作原理如下:

    public void dijkstra(int s, int t) { // 從頂點 s 到頂點 t 的最短路徑
        int[] predecessor = new int[this.v]; // 用來還原最短路徑
        Vertex[] vertexes = new Vertex[this.v]; // 記錄起始頂點到這個頂點的距離
        for (int i = 0; i < v; ++i) { // 初始化 dist 爲無窮大
            vertexes[i] = new Vertex(i, Integer.MAX_VALUE);
        }
        PriorityQueue<Vertex> queue = new PriorityQueue<>(); // 小頂堆
        boolean[] inQueue = new boolean[this.v]; // 標記是否進入過隊列
        queue.add(vertexes[s]); // 先把起始頂點放到隊列中
        vertexes[s].dist = 0;
        inqueue[s] = true;
        while (!queue.isEmpty()) {
            Vertex minVertex= queue.poll(); // 取 dist 最小的頂點
            if (minVertex.id == t) break; // 最短路徑產生了
            for (int i = 0; i < adj[minVertex.id].size(); ++i) {
                Edge e = adj[minVertex.id].get(i); // 取出一條 minVetex 相連的邊
                Vertex nextVertex = vertexes[e.tid]; // minVertex-->nextVertex
                // 找到一條到 nextVertex 更短的路徑
                if (minVertex.dist + e.w < nextVertex.dist) {
                    nextVertex.dist = minVertex.dist + e.w; // 更新 dist
                    predecessor[nextVertex.id] = minVertex.id; // 更新前驅頂點
                    if (inqueue[nextVertex.id] == false) { // 如果沒有在隊列中
                        queue.add(nextVertex); // 就把它放到隊列中
                        inqueue[nextVertex.id] = true;
                    }
                }
            }
        }
        // 輸出最短路徑
        System.out.print(s);
        print(s, t, predecessor);
    }
    private void print(int s, int t, int[] predecessor) {
        if (s == t) return;
        print(s, predecessor[t], predecessor);
        System.out.print("->" + t);
    }

用 vertexes 數組,記錄從起始頂點到每個頂點的距離(dist)。起初,我們把所有頂點的 dist 都初始化爲無窮大(也就是代碼中的 Integer.MAX_VALUE)。我們把起始頂點的 dist 值 初始化爲 0,然後將其放到優先級隊列中。 我們從優先級隊列中取出 dist 最小的頂點

minVertex,然後考察這個頂點可達的所有頂點(代 碼中的 nextVertex)。如果 minVertex 的 dist 值加上 minVertex 與 nextVertex 之間邊的權 重 w 小於 nextVertex 當前的 dist 值,也就是說,存在另一條更短的路徑,它經過 minVertex 到達 nextVertex。那我們就把

nextVertex 的 dist 更新爲 minVertex 的 dist 值加上 w。然 後,我們把 nextVertex 加入到優先級隊列中。重複這個過程,直到找到終止頂點 t 或者隊列爲 空。 以上就是 Dijkstra 算法的核心邏輯。除此之外,代碼中還有兩個額外的變量,predecessor 數 組和 inqueue 數組。

predecessor 數組的作用是爲了還原最短路徑,它記錄每個頂點的前驅頂點。最後,我們通過 遞歸的方式,將這個路徑打印出來。這個跟圖的搜索中打印路徑方法一樣。

inqueue 數組是爲了避免將一個頂點多次添加到優先級隊列中。更新了某個頂點的 dist 值 之後,如果這個頂點已經在優先級隊列中了,就不要再將它重複添加進去了。

 

Dijkstra 算法的時間複雜度

在剛剛的代碼實現中,最複雜就是 while 循環嵌套 for 循環那部分代碼了。while 循環最多會執 行 V 次(V 表示頂點的個數),而內部的 for 循環的執行次數不確定,跟每個頂點的相鄰邊的 個數有關,分別記作 E0,E1,E2,……,E(V-1)。

如果把這 V 個頂點的邊都加起來, 最大也不會超過圖中所有邊的個數 E(E 表示邊的個數)。 for 循環內部的代碼涉及從優先級隊列取數據、往優先級隊列中添加數據、更新優先級隊列中的 數據,這樣三個主要的操作。

優先級隊列是用堆來實現的,堆中的這幾個操作,時間 複雜度都是 O(logV)(堆中的元素個數不會超過頂點的個數 V)。 所以,綜合這兩部分,再利用乘法原則,整個代碼的時間複雜度就是 O(E*logV)。

如何計算最優出行路線?

從理論上講,用 Dijkstra 算法可以計算出兩點之間的最短路徑。但是對於一 個超級大地圖來說,岔路口、道路都非常多,對應到圖這種數據結構上來說,就有非常多的頂點 和邊。如果爲了計算兩點之間的最短路徑,在一個超級大圖上動用 Dijkstra 算法,遍歷所有的 頂點和

邊,顯然會非常耗時。

做工程不像做理論,一定要給出個最優解。理論上算法再好,如果執行效率太低,也無法應用到 實際的工程中。對於軟件開發工程師來說,我們經常要根據問題的實際背景,對解決方案權衡取 舍。類似出行路線這種工程上的問題,我們沒有必要非得求出個絕對最優解。很

多時候,爲了兼 顧執行效率,我們只需要計算出一個可行的次優解就可以了。

優化方案

雖然地圖很大,但是兩點之間的最短路徑或者說較好的出行路徑,並不會很“發散”,只會出現在兩點之間和兩點附近的區塊內。所以可以在整個大地圖上,劃出一個小的區塊,這個小區塊恰好可以覆蓋住兩個點,但又不會很大。只需要在這個小區塊內部運行 Dijkstra 算

法, 這樣就可以避免遍歷整個大圖,也就大大提高了執行效率。

如果兩點距離比較遠,從北京海淀區某個地點,到上海黃浦區某個地點,那 上面的這種處理方法,顯然就不工作了,畢竟覆蓋北京和上海的區塊並不小。

對於這樣兩點之間距離較遠的路線規劃,可以把北京海淀區或者北京看作一個頂點,把上海黃浦區或者上海看作一個頂點,先規劃大的出行路線。比如如何從北京到上海,必須要經過某幾個頂點,或者某幾條幹道,再細化每個階段的小路線。 這樣,最短路徑問題就解決了

最少時間和最少紅綠燈

最短路徑問題時,每條邊的權重是路的長度。在計算最少時間的時候,算法還是不變,只需要把邊的權重,從路的長度變成經過這段路所需要的時間。不過,這個時間會根據擁堵情況時刻變化。如何計算車通過一段路的時間呢?

每經過一條邊,就要經過一個紅綠燈。關於最少紅綠燈的出行方案,實際上,我們只需要把每條邊的權值改爲 1 即可,算法還是不變,繼續使用Dijkstra算法。不過,邊的權值爲1,也就相當於無權圖了,還可以使用廣度優先搜索算法。

廣度優先搜索算法計算出來的兩點之間的路徑,就是兩點的最短路徑。

真實的地圖軟件的路徑規劃,要比這個複雜很多;比起Dijkstra 算法,地圖軟件用的更多的是類似 A* 的啓發式搜索算法,不過也是在 Dijkstra 算法上的優化罷了。

總結

一種非常重要的圖算法,Dijkstra 最短路徑算法。實際上,最短路徑算法還有很多,比如 Bellford 算法、Floyd 算法等等。

 Dkijstra 算法不是貪心算法,它是動態規劃算法,求得的解全局最優解。

廣度優先算法遍歷無權圖中一點s到另 一點t的路徑,就是它的最短路徑。

在計算最短時間的出行路線中,如何獲得通過某條路的時間呢?這個題目很有意思;

獲取通過某條路的時間:通過某條路的時間與①路長度②路況(是否平坦等)③擁堵情況④紅 綠燈個數等因素相關。獲取這些因素後就可以建立一個迴歸模型(比如線性迴歸)來估算時間。

其中①②④因素比較固定,容易獲得。③是動態的,但也可以通過

  • a.與交通部門合作獲得路段 擁堵情況;
  • b.聯合其他導航軟件獲得在該路段的在線人數;
  • c.通過現在時間段正好在次路段的 其他用戶的真實情況等方式估算。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章