單源最短路徑和廣度優先搜索要做的事很像。
關於廣度優先搜索可以看圖算法這一篇筆記。
單源最短路徑給定一個源s,當算法執行完畢,找出從源s到圖中的每個頂點權重最小的一條路徑。
其實廣度優先搜索可以看作特殊情況的單源最短路徑,在廣度優先搜索解決的圖中,所有的邊權重都為1。
注意: 本篇筆記說的最短路徑都是指權重最低的路徑,並非通過的邊數最少的路徑
負權重
某些單源最短路徑問題的算法中允許出現負權重的邊,就比如我們要討論的Bellman-Ford
算法。
但是有些算法不允許,比如Dijkstra算法。
關於環路
單源最短路徑問題中能不能存在環路呢?我們分情況討論。
- 權值為正的環路
我們看s到c的路徑,這個路徑包含一個環路,即節點c和節點d。我們從s出發,到c之后還可以走無數次c和d構成的環路,最終都能回到c。但是由於我們每從c走到d,權重就會加6,再從d走回c會減3,所以每次繞環是需要額外花費3的權重的。所以我們最終的找出的最短路徑中不可能存在權值為正的環路。 - 權值為負的環路
看s到e的路徑,和上面的分析一樣,每次繞e和f組成的環,會減去3的權值,所以這種情況已經不存在最短(權重最低)路徑了,因為每次繞環權值都會更小,所以最短的路徑應該是負無窮。這種情況算法得不到正確的答案。我們的Bellman-Ford算法會檢測到負值環,並返回False通知調用者。 - 權值為零的環路
這種情況無論繞環走多少次都無影響,所以我們可以重復刪除這些環路,最后得到一條不包括環路的最短路徑。
綜上所述,算法最后找到的最短路徑中應該不包含環路,即都是簡單路徑。
松弛操作
我們來看下單源最短路徑算法的核心操作。松弛。
對於每個節點v,維持一個屬性v.d
,這個屬性用來維持源s到v的最短路徑估值。它是s到v的一條最短路徑權重的上界。
對於每個節點v,維持一個屬性v.PI
,這個屬性和d配合,用於記錄當前的估值情況下和v連接的前驅節點。
提供一個權重函數w(u,v)
計算u和v構成的邊的權重
我們給每個節點的d和PI初始值定為無窮和NIL。把源節點s的d定為0
INITIALIZE-SINGLE-SOURCE(G,s)
for 圖G中的每個節點 v
v.d = ∞
v.PI = NIL
s.d = 0
提供松弛函數RELAX(u,v,w)
對邊(u,v)
進行松弛,參數w是權重函數。
松弛函數其實就是測一下如果經過u的話會不會對節點v的最短路徑估值d起到優化。在說通俗點就是測試連接(u,v)
的話會不會讓當前s到v的最短路徑估值更小。如果是就連接(u,v)
RELAX(u,v,w)
if v.d > u.d + w(u,v)
v.d = u.d + w(u,v)
v.PI = u
對溜,就是這樣,如果當前節點v的估值大於u的估值加邊(u,v)
的權重的話就重置v的估值為u.d+w(u,v)
並設置v的前驅節點為u。
最短路徑和松弛操作性質
我們假設sp(s,v)
是從源s到節點v的最短路徑的權重
- 三角不等式性質: 對於任何邊
(u,v)
我們有sp(s,v) <= sp(s,u) + w(u,v)
啥意思呢,就是說明對於每個邊,要么就是最短路徑里需要走這條邊,這時不等式中的小於號拿掉,變成等式。要么就是可以有一條不經過`(u,v)的更短的路徑通往v。 - 上界性質:對於所有節點v,總有
v.d >= sp(s,v)
。
這是一條關於松弛操作的性質,我們不斷調用松弛操作去調節節點v
的最短路徑估值d,一旦發現更短的路徑就更新它,直到v.d == sp(s,v)
后,松弛操作將永遠找不到更短的路徑,v.d
就永遠不會更新了。 - 非路徑性質:如果s到v之間不存在路徑,則
v.d == sp(s,v) == ∞
。
很正常,如果不存在路徑,那么v的估值永遠得不到更新,一直是無窮。 - 收斂性質:對於某些節點u,v,如果
s⇝u→v
是圖中的一條最短路徑,並且在對邊(u,v)
進行松弛前的任意時間有u.d == sp(s,u)
,則在之后的所有時間有v.d == sp(s,v)
。 - 路徑松弛性質:如果
p=(v0,v1,...,vk)
是從源節點s=v0
到節點vk
的一條最短路徑,並且對p中邊的松弛次序為(v0,v1),(v1,v2)...(vk-1,vk)
,則vk.d == sp(s,vk)
。該性質的成立與其他任何松弛操作無關,即使這些松弛操作是和對p上的邊所進行的松弛操作穿插進行的。
因為p是最短路徑,所以每次按上面順序進行松弛估值一定會更新到最短的路徑啊。毋庸置疑。這條和第四條差不多啊。 - 前驅子圖性質:如對於所有節點v,一旦
v.d == sp(s,v)
則前驅子圖是一顆以跟節點為s的最短路徑樹。
這性質在“關於環路”那個段落里其實已經隱含了,因為最短路徑一定是不包含環路的簡單路徑,所以肯定是一棵樹。
不好意思題外話。。。🐎了個🥚,抄書好累啊,但是我不得不把算法導論翻譯成人話,沒錯我在逼自己看懂它。。。
有了上面的分析,我們可以開始研究主角Bellman-Ford算法了
Bellman-Ford算法
Bellman-Ford算法可以工作在不存在從源節點可以達到的權重為負值的環路的圖。
如果存在這樣的圖,算法將返回False通知用戶無法給出正確可靠的結果。
算法通過不斷對每條邊進行松弛來一點點的使每個節點v的最短路徑估值d一點一點的下降,最終達到sp(s,v)
。
其實對於每一次對所有邊挨個進行松弛操作,必定會有至少一條邊的d值會被設置為正確的。這樣只需要對圖的每條邊進行圖中節點數減1次循環就可以計算出每個節點正確的最短路徑估值。所以我們這樣編寫BELLMAN-FORD算法:
BELLMAN-FORD(G,w,s)
INITIALIZE-SINGLE-SOURCE(G,s)
for i = 1 to |G.V| - 1
for G中每個邊 (u,v)
RELAX(u,v,w)
for G中每個邊 (u,v)
if v.d > u.d + w(u,v)
return FALSE
return TRUE
But,,why???下面證明下。
算法能得到正確估值的證明
假設圖不包含從源節點s可以到達的權重為負值的環路,我們考慮每一個可以從源到達的節點v肯定有一條最短路徑p={v1,v2,...,vk}
。s=v1,v=vk。由於我們前面的前驅子圖性質,所以這個最短路徑肯定是一個簡單路徑,即沒有環。那么這條路徑里就最多有圖中節點數量-1個邊。再根據前面的路徑松弛性質,只要這條最短路徑是按順序松弛的,那么就可以得到正確的d值,和其它松弛操作的順序無關,那么每次外層for循環都松弛所有的邊,那么在第i次松弛操作時被松弛的邊肯定包含(vi-1,vi)
,所以,v.d==vk.d==sp(s,vk)==sp(s,v)
d就是對的嘍。
迄今為止,我們還有一個坑沒有解決。那就是算法的返回值。
算法是否會在存在負權環路時返回FALSE,在正常情況下返回TRUE呢?
算法能得到正確返回值的證明
先看看算法導論的證明,確實很嚴謹很正確。不過我下次復習看這個筆記的時候。。。我這笨腦袋肯定還得想半天。
所以得想想怎么說才能讓我下次一下就能懂。
emmm。。。看這。。。。圖???假設有這么一個負權環,從上面的節點開始,假設初始狀態那個頂上的節點的d是0,在第一圈到左下腳的節點時,還是滿足v.d<=u.d+w(u,v)
的。不會返回FALSE,不過當它轉了一圈后再來就有問題了。會產生v.d>u.d+w(u,v)
。而且隨着圈越轉越多這個差值會越來越離譜。
每次遍歷所有邊就必定要走一遍環路。所以到最后只需檢查v.d>u.d+w(u,v)
即可。算法是正確的