這裏感謝百度文庫,百度百科,維基百科,還有算法導論的做者以及他的小夥伴們......前端
最短路是現實生活中很常見的一個問題,以前練習了不少BFS的題目,BFS能夠暴力解決不少最短路的問題,可是他有必定的侷限性,該算法只能用於無權重即權重爲單位權重的圖,那麼下面咱們會介紹五種用途更普遍的算法......算法
最短路徑的幾個變體數組
單源最短路徑問題:咱們但願找到從源結點s到其它全部結點的最短路徑。閉包
單目的地最短路徑問題:找到從每一個結點v到目的u的最短路徑,若是將圖中每條邊的方向翻轉過來,咱們就能夠將這個問題轉換爲單源最短路徑問題。函數
單結點對最短路徑問題:該類問題的求解方法和單源最短路徑類似,相比之下還有更多的優化算法。oop
全部結點對的最短路徑問題:對於每一個結點u和v,找到從結點u到v的最短路徑。雖然能夠對每一個結點運行一次單源最短路徑算法,可是咱們有更好的算法來解決這類問題,就是後面講到的Floyd-Warshall算法。性能
最短路知足最優子結構,最優子結構是運用貪心算法和動態規劃的一個重要指標,咱們即將瞭解的Dijkstra算法就是一個貪心算法,Floyd-Warshall算法就是一個動態規劃算法,下面咱們貼出一段算法導論上面對該定理的證實。優化
通常用於解決單源最短路問題,這裏權值能夠爲負值,給定一個有向圖G(V, E)和一個權重函數w : E -> R,該算法返回一個布爾值,以代表是否存在一個從源結點能夠到達的權重爲負值的環路。若是存在這樣的迴路,spa
該算法將告訴咱們不存在解決方案,不然該算法會給出最短路徑和他們的權重。3d
該算法經過對邊進行鬆弛操做來逐漸下降從源結點到每一個結點v的最短路徑的估計值v.d,直到得到最短路徑爲止。
下面給出算法導論上面的僞代碼,因爲在進行該算法以前要對每一個結點的初始狀態進行初始化,因此給出一個初始化操做。
初始化操做就是對於每一個結點,將其到全部結點的最短路徑初始化爲正無窮,將他們的父親結點初始化爲NULL,並將源結點到本身的最短路徑初始化爲0。
鬆弛操做就是對於每條邊,若是發現當前邊終點處對應的最短路徑v.d比起點處u.d + w(u, v) 大,則將其更新,因爲最短路徑問題知足最優子結構,並在最短路徑樹中將u記錄爲v的父親結點。
整個Bellman-Ford算法就是先對圖中的結點進行初始化,而後對於每一個結點v進行鬆弛操做,鬆弛時對每一個v爲起點的邊進行鬆弛操做,以後再檢查是否知足最短路問題的最優子結構性質,若是發現違背這個性質,則說明圖中存在負權環。
下面咱們介紹一種Bellman-Ford算法的優化算法SPFA算法......
SPFA算法全稱爲Shortest Path Fast Algorithm,在1994年由西南交通大學段凡丁提出,與Bellman-Ford算法同樣,用於求解含負權的最短路問題以及判斷是否存在負權環。在不含負權環的題狀況下優先選擇堆優化的Dijkstra算法求最短路徑,這就避免SPFA出現最壞的狀況。SPFA算法的基本思路與Bellman-Ford算法相同,即每一個節點都被用做用於鬆弛其相鄰節點的備選節點。相較於Bellman-Ford算法,SPFA算法的提高在於它並不盲目嘗試全部節點,而是維護一個備選節點隊列,而且僅有節點被鬆弛後纔會放入隊列中。整個流程不斷重複直至沒有節點能夠被鬆弛。
下面咱們給出維基上SPFA算法的僞代碼:
// w(u,v)是邊(u,v)}的權。 procedure Shortest-Path-Faster-Algorithm(G, s) 1 for each vertex v ≠ s in V(G) 2 d(v) := ∞ 3 d(s) := 0 4 offer s into Q 5 while Q is not empty 6 u := poll Q 7 for each edge (u, v) in E(G) 8 if d(u) + w(u, v) < d(v) then 9 d(v) := d(u) + w(u, v) 10 if v is not in Q then 11 offer v into Q
SPFA算法的性能很大程度上取決於用於鬆弛其餘節點的備選節點的順序。事實上,若是Q是一個優先隊列,則這個算法將極其相似於Dijkstra算法。然而儘管這一算法中並無用到優先隊列,仍有兩種可用的技巧能夠用來提高隊列的質量,而且藉此可以提升平均性能(但仍沒法提升最壞狀況下的性能)。兩種技巧經過從新調整Q中元素的順序從而使得更靠近源點的節點可以被更早地處理。所以一旦實現了這兩種技巧,Q將再也不是一個先進先出隊列,而更像一個鏈表或雙端隊列。
下面咱們說一下兩種優化的思路以及維基上提供的僞代碼:
Small Lable First :他的意思是距離小者優先,也就是咱們有入隊元素時,咱們判斷隊尾元素的權值是否小於隊頭元素權值,若是知足則將隊尾元素剔除並將插到隊首。
下面咱們給出維基百科上的僞代碼,把這一段代碼加到上述SPFA代碼的第11行以後便可:
1 procedure Small-Label-First(G, Q) 2 if d(back(Q)) < d(front(Q)) then 3 u := pop back of Q 4 push u into front of Q
Large Lable Last:這種優化思路是距離大者置後,也就是咱們每次都會計算出當前隊列中元素的平均值,當發現正在入隊的元素的權值大於平均權值時就將它從隊首剔除並插入到隊尾。
下面咱們給出維基百科上的僞代碼,把這一段代碼加到上述SPFA代碼的第11行以後便可:
1 procedure Large-Label-Last(G, Q) 2 x := average of d(v) for all v in Q 3 while d(front(Q)) > x 4 u := pop front of Q 5 push u to back of Q
上述算法便是單源最短路含負權問題的通常解法,下面咱們描述一種對於有向無環圖上的最短路問題的解法,該解法內求解單源最短路問題的時間複雜度爲O(V + E)。
在有向無環圖中,即便某個邊的權重爲負值,若是圖中不存在起點s可到達的權重爲負值的環,那麼其最短路徑都是存在的。
咱們的算法首先對有向無環圖進行拓撲排序,以便肯定結點之間的一個線性次序。若是圖中包含從u到v的一條最短路,那麼在有向無環圖的拓撲序中,u結點必定位於v結點的前面。所以咱們只須要按照拓撲序 對結點進行一遍處理便可。
每次對一個結點進行處理時,咱們對從該結點出發的全部邊進行鬆弛操做。
鬆弛操做和初始化操做與Bellman-Ford算法中的僞代碼徹底同樣,下面給出算法導論上面關於拓撲排序和DAG-SHORTEST-PATHs的僞代碼:
算法導論中的拓撲排序依靠對於每一個邊記錄DFS完成順序的結果進行的,因此咱們對該DFS和DFS造成的拓撲序進行講解。
對於每一個結點,用白色表示它尚未訪問過,用灰色表示正在訪問,用黑色表示訪問完畢,那麼對於任意一個圖,咱們首先對圖中的結點進行初始化,將全部結點的父親結點初始化爲NULL,將全部結點的顏色都初始化爲白色表示未
訪問,接着順序遍歷每一個結點,若是結點沒有被訪問果咱們就訪問該結點,訪問每一個結點時,咱們設置一個全局變量的計時器存儲每一個結點被訪問完畢的時間,咱們對該結點的其它鄰接結點進行訪問,若是鄰接結點爲白色則對其進行相同
的訪問,並將它的父親標記爲當前結點,訪問完一個結點的全部鄰接結點時咱們將其狀態變爲黑色,並記錄此時的訪問完成時間用於拓撲排序。
如何求出拓撲序呢,首先利用DFS計算出每一個結點最後一次訪問的時間,對於圖中的每一個結點,訪問完畢後直接將其結點的編號加入到鏈表的最前端,最後返回頭結點。那麼要如何證實一個DFS對於一個結點訪問完畢的順序就是逆拓撲序呢?
首先咱們知道,對於一個圖G,若是他是有向無環圖,那麼圖中必定存在出度爲0的結點,這個出度爲零的結點一定是最後訪問的。那麼在DFS訪問的過程當中,若是一個結點的全部子節點都被訪問完畢以後,其出度即爲零,即DFS對結點的
訪問完畢順序知足逆拓撲序。
那麼要如何實現DAG-SHORTEST-PATHS呢,咱們先將全部結點拓撲排序,接着對結點的信息狀態進行初始化,接着按照拓撲序對每一個結點的鄰接結點進行鬆弛操做,便可完成。
Dijkstra算法解決的是帶權重的有向圖上單源最短路問題,該算法要求全部邊的權重都爲非負值。若是方式適當,Dijkstra算法的運行時間要優於SPFA算法的運行時間。
Dijkstra算法會維護一組關鍵的信息,用於下來的運算,從源結點s到該集合中每一個結點的最短路徑已經被找到,算法重複從V-S中選擇最短路徑估計最小的結點u,將u加入到集合S中,而後對全部從u出發的邊進行鬆弛操做。
該算法之因此是正確的是由於每次選擇結點u加入S中時,有u.d = shortest-paths。那麼要如何證實在結點u加入集合s時有上述性質呢?......算法導論上面花了較長的篇幅進行證實,這裏再也不進行贅述。
這裏是用最小優先隊列Q來保存結點集合。首先咱們對每一個結點進行相同於Bellman-Ford算法的初始化,接着將集合s初始化爲一個空集,算法第11行將全部結點放入該隊列中,對於該隊列中的全部結點,咱們每次訪問其中
權值最小的那個結點u,並將其加入S中,接着對其結點進行鬆弛,鬆弛操做與Bellman-Ford算法相同。
下面給出維基百科上Dijkstra算法關於優先隊列優化的僞代碼:
1 function Dijkstra(Graph, source): 2 dist[source] ← 0 // Initialization 3 4 create vertex set Q 5 6 for each vertex v in Graph: 7 if v ≠ source 8 dist[v] ← INFINITY // Unknown distance from source to v 9 prev[v] ← UNDEFINED // Predecessor of v 10 11 Q.add_with_priority(v, dist[v]) 12 13 14 while Q is not empty: // The main loop 15 u ← Q.extract_min() // Remove and return best vertex 16 for each neighbor v of u: // only v that are still in Q 17 alt ← dist[u] + length(u, v) 18 if alt < dist[v] 19 dist[v] ← alt 20 prev[v] ← u 21 Q.decrease_priority(v, alt) 22 23 return dist, prev
上面的代碼和算法導論給出的僞代碼思路相同,可是實現方式有必定的差距,該算法和算法導論中Dijkstra算法僞代碼不一樣的地方爲,該算法第一次只將起始點加入優先隊列,而後在每次進行鬆弛操做的時候將正在鬆弛的點加入優先隊列用於計算,算法導論中僞代碼是將全部頂點先加入優先隊列,因爲剛開始時只有源點s的權值爲0,因此該點就是所採用的第一個點,接着循環進行鬆弛操做時改變相應的值,因爲優先隊列會從新平衡,因此這樣也並沒有大礙,可是建議仍是採用上面代碼而非一次將全部結點都加入優先隊列,由於這會使得該算法變得笨重些許......(堆每次須要調整使本身平衡)。下面給出算法導論中對於Dijkstra算法的僞代碼實現。
Floyd-Warshall算法經過逐步改進兩個頂點之間的最短路徑來實現,直到估計是最優的。
是解決任意兩點間的最短路徑的一種算法,能夠正確處理有向圖或負權(但不可存在負權迴路)的最短路徑問題,同時也被用於計算有向圖的傳遞閉包[2]。
Floyd-Warshall算法的時間複雜度爲O(N3),空間複雜度爲O(N2)。
原理:Floyd-Warshall算法的原理是動態規劃。
設D(i, j, k)爲從i到j的只以(1....k)集合中的節點爲中間節點的最短路徑的長度。
所以,D(i, j, k) = min(, D(i, j, k - 1), D(i, k, k - 1) + D(k, j, k - 1));。
在實際算法中,爲了節約空間,能夠直接在原來空間上進行迭代,這樣空間可降至二維。
下面給出維基百科上面的僞代碼描述:
用一個next數組保存路徑,意味着以結點 i 爲開頭的一顆最短路徑樹。
1 let dist be a |V| * |V| array of minimum distances initialized to 2 let next be a |V| * |V| array of vertex indices initialized to null 3 4 procedure FloydWarshallWithPathReconstruction () 5 for each edge (u,v) 6 dist[u][v] ← w(u,v) // the weight of the edge (u,v) 7 next[u][v] ← v 8 for each vertex v 9 dist[v][v] ← 0 10 next[v][v] ← v 11 for k from 1 to |V| // standard Floyd-Warshall implementation 12 for i from 1 to |V| 13 for j from 1 to |V| 14 if dist[i][j] > dist[i][k] + dist[k][j] then 15 dist[i][j] ← dist[i][k] + dist[k][j] 16 next[i][j] ← next[i][k]
後續還會更新有關Johnson算法得有關內容,而且更新Floyd-Warshall算法得內容。
這幾天會有持續的最短路題目更新emmm.....