【轉載】Folyd演算法理解
一篇非常優秀的解析
floyd演算法:我們真的明白floyd嗎?
2016年08月02日 20:14:18 ljhandlwt 閱讀數:7444 個人分類: 演算法圖論裡一個很重要的問題是最短路徑問題.
這個問題,在離散數學課上會考,資料結構與演算法課上會考,圖論課上會考,計算機網路裡會考....
解決最短路徑問題有幾個出名的演算法:
1.dijkstra演算法,最經典的單源最短路徑演算法
2.bellman-ford演算法,允許負權邊的單源最短路徑演算法
3.spfa,其實是bellman-ford+佇列優化,其實和bfs的關係更密一點
4.floyd演算法,經典的多源最短路徑演算法
今天我們討論的是floyd演算法,它用於解決多源最短路徑問題,演算法時間複雜度是O(n3).
floyd演算法為什麼經典,因為它只有5行(或者4行)!!!
是的,沒有特意的寫成一行的程式碼.
這個演算法短的離譜,以致於我們通常直接將它背了下來當模板使用,而不像學dijkstra那時候一步步理解它是如何貪心的.
那麼,為什麼floyd演算法是這個樣子的呢?或者說,為什麼這樣就能求出所有點到所有點的最短路徑?
談起floyd演算法,一般我們會說這是一個動態規劃演算法.(怪不得如此優美)
為什麼是個動態規劃演算法?因為它有遞推公式:d[i][j]=min(d[i][j],d[i][k]+d[k][j])
還有一點就是三重迴圈,k要寫外面,裡面的i,j是對稱的,隨便巢狀沒所謂.
這大概就是我們大部分人對floyd演算法的瞭解.
那麼,我們其實沒有解決核心問題,為什麼這樣就能解決問題,為什麼是這個遞推公式,是這個巢狀順序?
這一切都不像學長所說的那麼顯然...
事實上,如果你明白了bellman-ford的正確性,你就會明白為什麼floyd是可行的了.
在這裡我們不討論floyd以外的演算法,我們正面剛floyd.
floyd的最關鍵的地方是它的遞推公式,它的遞推公式寫得抽象一點就是下圖:
簡單來說,這個i到j的最短路徑,我們可以找一箇中間點k,然後變成子問題,i到k的最短路徑和k到j的最短路徑.
也就是說,我們可以列舉中間點k,找到最小的d[i][k]+d[k][j],作為d[i][j]的最小值.
這好像很合理啊,假如所有d[i][k]和d[k][j]都取了最小值的話,這個dp很dp.
但是,d[i][k]和d[k][j]一開始都不一定取了最小值的啊!它們和d[i][j]一樣,會不斷變小.
那麼,會不會存在這種情況,d[i][j]取最小值時的k是某個x.
而在最外迴圈k=x的時候,d[i][x]或者d[x][j]並沒有取到最小值,但這個時候會執行d[i][j]=min(d[i][j],d[i][x]+d[x][j]),造成了d[i][j]並不能取到真正的最小值.
答案當然是,並不會出現這種情況.我們今天的重點就是來討論為什麼不會出現這種情況.
我們需要證明一個很致命的結論:
假設i和j之間的最短路徑上的結點集裡(不包含i,j),編號最大的一個是x.那麼在外迴圈k=x時,d[i][j]肯定得到了最小值.
怎麼證明,可以用強歸納法.
設i到x中間編號最大的是x1,x到j中間編號最大的是x2.
由於x是i到j中間編號最大的,那麼顯然x1<x,x2<x.
根據結論,k=x1的時候d[i][x]已經取得最小值,k=x2的時候d[x][j]已經取得最小值.
那麼就是k=x的時候,d[i][x]和d[x][j]肯定都已經取得了最小值.
因此k=x的時候,執行d[i][j]=min(d[i][j],d[i][x]+d[x][j])肯定會取得d[i][j]的最小值.
證畢.
用強歸納法證明固然優美,但是顯得有點抽象,並且我們忽略了一些初始情況和特殊情況(比如i和j之間沒有結點).
現在,我們舉一個實際的例子,去說明它的正確性.
上圖是1到5的最短路徑,這意味著d[1][2],d[2][4],d[4][3],d[3][5]在一開始就是最小值了.
這在某種程度上證明了我們那個結論,因為中間無結點,相當於最大編號是-∞,就是k=-∞,即一開始的時候就取了最小值了.
首先第一輪k=1,不難知道,1到5這些點之間原本沒能取得最短距離的,更新後也沒能保證取得最短距離.
第二輪k=2,我們發現d[1][4]肯定取得了最小值,因為會執行d[1][4]=min(d[1][4],d[1][2]+d[2][4]),而d[1][2]和d[2][4]已經是最小值.
第三輪k=3,我們發現d[4][5]肯定取得了最小值.
第四輪k=4最關鍵,我們發現d[2][3],d[1][3],d[2][5],d[1][5]都肯定取得了最小值.
d[2][3]=d[2][4]+d[4][3]
d[1][3]=d[1][4]+d[4][3]
d[2][5]=d[2][4]+d[4][5]
d[1][5]=d[1][4]+d[4][5]
我們可以看到,等號右邊的幾個值,都在k=4之前取得了最小值.
這意味著d[1][3]的更新就是最小的了,不會存在d[1][4]未取最小值導致d[1][3]未取得最小的情況發生.
並且,我們看到1到4之間的最大編號是2,而d[1][4]在k=2時肯定取得了最小值,後面的也是同理.
這在感性上證明了我們那個致命的結論.
有了這個致命的結論,根據一開始的推理,其實已經可以顯然地理解為什麼floyd是正確的了.
事實上,假如在執行d[i][j]=min(d[i][j],d[i][k]+d[k][j])前,對於所有的k,d[i][k]和d[k][j]都是最小值,那麼上面例子裡d[1][5]之間的k可以選擇2,3,4.
但是,我們沒法做到對於所有的k,執行那個語句前d[i][k]和d[k][j]都是最小值.
但是,我們保證了能存在一個k=x,在執行那個語句前d[i][x]和d[x][j]都是最小值.
而這個x,是i和j最短路徑的點集裡最大的編號.
這也說明了為什麼k一定要是在最外層的原因,
因為假如k在最裡層,那麼d[i][j]=min(d[i][j],d[i][k]+d[k][j])是一次性執行完.
那麼我們就要保證,在這時候,至少存在一個k=x,使得d[i][x]和d[x][j]都是取得了最小值.
然而在這種情況下我們並不能保證,但如果k在最外層就可以保證了.