floyd–warshall(簡稱floyd演算法)是一種著名的解決任意兩點間的最短路徑(all paris shortest paths,apsp)的演算法。從表面上粗看,floyd演算法是乙個非常簡單的三重迴圈,而且純粹的floyd演算法的迴圈體內的語句也十分簡潔。我認為, 正是由於「floyd演算法是一種動態規劃(dynamic programming)演算法」的本質,才導致了floyd演算法如此精妙。因此,這裡我將從floyd演算法的狀態定義、動態轉移方程以及滾動陣列等重要方 面,來簡單剖析一下圖論中這一重要的基於動態規劃的演算法——floyd演算法。
在動態規劃演算法中,處於首要位置、且也是核心理念之一的就是狀態的定義。在這裡,把d[k][i][j]定義成:
「只能使用第1號到第k號點作為中間媒介時,點i到點j之間的最短路徑長度。」
圖中共有n個點,標號從1開始到n。因此,在這裡,k可以認為是動態規劃演算法在進行時的一種層次,或者稱為「鬆弛操作」。d[1][i][j]表示 只使用1號點作為中間媒介時,點i到點j之間的最短路徑長度;d[2][i][j]表示使用1號點到2號點中的所有點作為中間媒介時,點i到點j之間的最 短路徑長度;d[n-1][i][j]表示使用1號點到(n-1)號點中的所有點作為中間媒介時,點i到點j之間的最短路徑長度d[n][i][j]表示 使用1號到n號點時,點i到點j之間的最短路徑長度。有了狀態的定義之後,就可以根據動態規劃思想來構建動態轉移方程。
動態轉移的基本思想可以認為是建立起某一狀態和之前狀態的一種轉移表示。 按照前面的定義,d[k][i][j]是一種使用1號到k號點的狀態,可以想辦法把這個狀態通過動態轉移,規約到使用1號到(k-1)號的狀態,即 d[k-1][i][j]。對於d[k][i][j](即使用1號到k號點中的所有點作為中間媒介時,i和j之間的最短路徑),可以分為兩種情況: (1)i到j的最短路不經過k;(2)i到j的最短路經過了k。不經過點k的最短路情況下,d[k][i][j]=d[k-1][i][j]。經過點k的 最短路情況下,d[k][i][j]=d[k-1][i][k]+d[k-1][k][j]。因此,綜合上述兩種情況,便可以得到floyd演算法的動態轉 移方程:
d[k][i][j] = min(d[k-1][i][j], d[k-1][i][k]+d[k-1][k][j])(k,i,j∈[1,n])
最後,d[n][i][j]就是所要求的圖中所有的兩點之間的最短路徑的長度。在這裡,需要注意上述動態轉移方程的初始(邊界)條件,即d[0] [i][j]=w(i, j),也就是說在不使用任何點的情況下(「鬆弛操作」的最初),兩點之間最短路徑的長度就是兩點之間邊的權值(若兩點之間沒有邊,則權值為inf,且我比 較偏向在floyd演算法中把圖用鄰接矩陣的資料結構來表示,因為便於操作)。當然,還有d[i][i]=0(i∈[1,n])。
這樣我們就可以編寫出最為初步的floyd演算法**:12
3456
78910
1112
void
floyd_original()
}
}
}
幾乎所有介紹動態規劃中最為著名的「0/1揹包」問題的演算法書籍中,都會進一步介紹利用滾動陣列的技巧來進一步減少演算法的空間複雜度,使得0/1背 包只需要使用一維陣列就可以求得最優解。而在各種資料中,最為常見的floyd演算法也都是用了二維陣列來表示狀態。那麼,在floyd演算法中,是如何運用 滾動陣列的呢?
再次觀察動態轉移方程d[k][i][j] = min(d[k-1][i][j], d[k-1][i][k]+d[k-1][k][j]),可以發現每乙個第k階段的狀態(d[k][i][j]),所依賴的都是前一階段(即第k-1階 段)的狀態(如d[k-1][i][j],d[k-1][i][k]和d[k-1][k][j])。
上圖描述了在前面最初試的floyd演算法中,計算狀態d[k][i][j]時,d[k-1]和d[k]這兩個二維陣列的情況 (d[k-1]表示第k-1階段時,圖中兩點之間最短路徑長度的二維矩陣;d[k]表示第k階段時,圖中兩點之間最短路徑長度的二維矩 陣)。紅色帶有箭頭的有向線段指示了規劃方向。灰色表示已經算過的陣列元素,白色代表還未算過的元素。由於d[k-1]和d[k]是兩個 相互獨立的二維陣列,因此利用d[k-1][i][j],d[k-1][i][k]和d[k-1][k][j](皆處於上方的二維陣列中)來計算d[k] [i][j]時沒有任何問題。
那如何利用乙個二維陣列來實現滾動陣列,以減小空間複雜度呢?
上圖是使用滾動陣列,在第k階段,計算d[i][j]時的情況。此時,由於使用d這個二維陣列作為滾動陣列,在各個階段的計算中被重複使 用,因此陣列中表示階段的那一維也被取消了。在這圖中,白色的格仔,代表最新被計算過的元素(即第k階段的新值),而灰色的格仔中的元素值,其實儲存的還 是上一階段(即第k-1階段)的舊值。因此,在新的d[i][j]還未被計算出來時,d[i][j]中儲存的值其實就對應之前沒有用滾動陣列時d[k- 1][i][j]的值。此時,動態轉移方程在隱藏掉階段索引後就變為:
d[i][j] = min(d[i][j], d[i][k]+d[k][j])
(k,i,j∈[1,n])
賦值號左側d[i][j]就是我們要計算的第k階段是i和j之間的最短路徑長度。在這裡,需要確保賦值號右側的d[i][j], d[i][k]和d[k][j]的值是上一階段(k-1階段)的值。前面已經分析過了,在新的d[i][j]算出之前,d[i][j]元素保留的值的確就 是上一階段的舊值。但至於d[i][k]和d[k][j]呢?我們無法確定這兩個元素是落在白色區域(新值)還是灰色區域(舊值)。好在有這樣一條重要的 性質,dp[k-1][i][k]和dp[k-1][k][j]是不會在第k階段改變大小的。也就是說,凡是和k節點相連的邊,在第k階段的值都不會變。 如何簡單證明呢?我們可以把j=k代入之前的d[k][i][j]=min(d[k-1][i][j], d[k-1][i][k]+d[k-1][k][j])方程中,即:
d[k][i][k]
= min(d[k-1][i][k], d[k-1][i][k]+d[k-1][k][k])
= min(d[k-1][i][k], d[k-1][i][k]+0)
= d[k-1][i][k]
也就是說在第k-1階段和第k階段,點i和點k之間的最短路徑長度是不變的。相同可以證明,在這兩個階段中,點k和點j之間的的最短路徑長度也是不 變的。因此,對於使用滾動陣列的轉移方程d[i][j] = min(d[i][j], d[i][k]+d[k][j])來說,賦值號右側的d[i][j], d[i][k]和d[k][j]的值都是上一階段(k-1階段)的值,可以放心地被用來計算第k階段時d[i][j]的值。
利用滾動陣列改寫後的floyd演算法**如下:12
3456
void
floyd()
因此,通過這篇文章的分析,我們可以發現,floyd演算法的的確確是一種典型的動態規劃演算法;理解floyd演算法,也可以幫助我們進一步理解動態規劃思想。
**:
floyd演算法和動態規劃
long long ago就已經知道了floyd演算法,關鍵 就4行,也容易記住,上上週又看到了floyd,都說是動態規劃,所以特意去學了一圈動態規劃,今天終於又回到了它 d k i j 定義 只能使用第1號到第k號點作為中間媒介時,點i到點j之間的最短路徑長度。在動態規劃演算法中,處於首要位置 且...
Floyd 動態規劃的理解
這裡我一直好奇的是這三個漂亮的迴圈是怎麼完成最終正確的結果的,如何證明它們是正確的。直到看了這裡的解析,才算弄懂了。floyd演算法是乙個經典的動態規劃演算法。用通俗的語言來描述的話,首先我們的目標是尋找從點i到點j的最短路徑。從動態規劃的角度看問題,我們需要為這個目標重新做乙個詮釋 這個詮釋正是動...
Floyd 動態規劃的理解
這裡我一直好奇的是這三個漂亮的迴圈是怎麼完成最終正確的結果的,如何證明它們是正確的。直到看了這裡的解析,才算弄懂了。floyd演算法是乙個經典的動態規劃演算法。用通俗的語言來描述的話,首先我們的目標是尋找從點i到點j的最短路徑。從動態規劃的角度看問題,我們需要為這個目標重新做乙個詮釋 這個詮釋正是動...