理解Dijkstra算法

學習過最短路徑問題的人都不會不知道Dijkstra算法。這個算法適用於解決無負權圖的單源最短路徑問題。這篇小文來談談如何理解這一算法。python

這裏首先給出一個Python 3的代碼實現(如下代碼出自Python Algorithms一書,略有改動)。算法

from heapq import heappush, heappop

def dijkstra(G, s):
    D, P, Q, C = {s: 0}, {s: None}, [(0, s, None)], set()
    while Q:
        du, u, p = heappop(Q)
        if u in C: continue
        C.add(u)
        D[u], P[u] = du, p
        for v in G[u]:
            if v not in C:
                heappush(Q, (du + G[u][v], v, u))
    return D, P

Dijkstra算法能夠從不少角度去理解。我我的以爲最便於理解和記憶的角度,是將其視爲一個圖遍歷算法。這從上面的代碼中就可以看出來。遍歷節點的算法都須要記錄「前線」,也就是全部已經訪問過但沒有「徹底考察」的節點。不一樣的遍歷算法,記錄「前線」所用的數據結構也不一樣。好比,BFS使用的是FIFO的隊列,DFS(暗中)使用的是FILO的棧,等等;而上面的代碼使用的則是一個優先隊列。(實際上,若是待解決的圖的全部邊的權重都爲同一正值的話,那麼該問題能夠用BFS來解決。換言之,BFS算法能夠視爲Dijkstra算法的一個特例。)數據結構

那麼,怎麼直觀地去理解Dijkstra算法所表示的這一遍歷過程呢?好比說,咱們要解決的問題若是能夠表現爲下面這幅圖——這是幅無向無負權圖,其中S點表示起點,每條邊的權重就是標註在旁邊的數字——那麼怎麼去表現Dijkstra算法所設定的遍歷呢?app

這裏須要耍一個思惟上的小花招:咱們把每條邊想象成真實的路徑,而後把權重想象成對應路徑的長度。而後假設站在起點S的是旋渦鳴人。在0時刻,旋渦鳴人首先將起點S塗成黑色(在代碼中表現爲加入C這個集合裏),而後沿每條從S點出發的路徑——在上圖裏就是SA、SB、SC三條邊——派出一名影分身。這些影分身的速度都同樣,每前進1個長度需花費1個時刻,因此在時刻七、九、14,這三個影分身分別依次到達C、B、A點。每一個影分身到達本身的目的地時,都會檢查該點有沒有被塗黑;若是已經被塗黑,說明有別的影分身走了一條更短的路徑來過這個點,那麼這個後來的影分身就完成了本身的使命,能夠「噗」地一下化成青煙消失了;若是沒有被塗黑,那麼說明這個影分身走的是起點S到該點的最短路徑,記下如今的時刻(也就是所循路徑的長度)以及上一個節點,把這一點塗黑,朝每條未被塗黑的鄰點派出新的影分身,而後本身就能夠消失掉了。例如上面的例子裏,時刻9時,有一個從起點S出發的影分身會到達B點。因爲B此時仍是白色的,說明S到B的最短路徑的長度爲9。塗黑B點,而後朝A、E兩點各派出一名影分身(之因此不往C點派影分身,是由於C點已經在時刻7被另外一個從S點出發的影分身塗黑了,也就是已經在C這個集合裏了)。注意從B點出發的影分身之一將在時刻11(=9+2)到達A點,早於從S點出發往A點去的那個影分身(他將在時刻14到達),因此等後者到達時,會發現A點已是黑色的了。如此重複,直到全部的影分身都消失了,也即全部從S點可以到達的點都塗成了黑色(遍歷),也記下了被塗黑的時刻(最短距離)和每個中間節點(最短路徑)。學習

按照這種理解,那麼算法中的優先隊列其實能夠看做是時間軸。每次往優先隊列裏push一個元素,表明新派出一個影分身;而每次從優先隊列裏pop出一個元素,則表明了某個影分身到達了其目的節點。並且,這種思路還能夠幫咱們從直觀上記憶Dijkstra算法的適用範圍:必須無負權邊,由於影分身所走路徑的長度不可能爲負;既能夠是有向圖也能夠是無向圖,影分身按照容許的方向走就好了。code

以上就是我對Dijkstra算法的一點理解。但願可以對學習算法的同窗有點幫助。隊列

相關文章
相關標籤/搜索