探求Floyd算法的動態規劃本質

Floyd–Warshall(簡稱Floyd算法)是一種著名的解決任意兩點間的最短路徑(All Paris Shortest Paths,APSP)的算法。從表面上粗看,Floyd算法是一個很是簡單的三重循環,並且純粹的Floyd算法的循環體內的語句也十分簡潔。我認爲, 正是因爲「Floyd算法是一種動態規劃(Dynamic Programming)算法」的本質,才致使了Floyd算法如此精妙。所以,這裏我將從Floyd算法的狀態定義、動態轉移方程以及滾動數組等重要方 面,來簡單剖析一下圖論中這一重要的基於動態規劃的算法——Floyd算法。html

在動態規劃算法中,處於首要位置、且也是核心理念之一的就是狀態的定義。在這裏,把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算法的動態轉 移方程:spa

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]code

最後,d[n][i][j]就是所要求的圖中全部的兩點之間的最短路徑的長度。在這裏,須要注意上述動態轉移方程的初始(邊界)條件,即d[0] [i][j]=w(i, j),也就是說在不使用任何點的狀況下(「鬆弛操做」的最初),兩點之間最短路徑的長度就是兩點之間邊的權值(若兩點之間沒有邊,則權值爲INF,且我比 較偏向在Floyd算法中把圖用鄰接矩陣的數據結構來表示,由於便於操做)。固然,還有d[i][i]=0i∈[1,n]htm

這樣咱們就能夠編寫出最爲初步的Floyd算法代碼:blog

1
2
3
4
5
6
7
8
9
10
11
12
void floyd_original() {
     for ( int i = 1; i <= n; i++)
         for ( int j = 1; j <= n; j++)
             d[0][i][j] = graph[i][j];
     for ( int k = 1; k <= n; k++) {
         for ( int i = 1; i <= n; i++) {
             for ( int j = 1; j <= n; j++) {
                 d[k][i][j] = min(d[k-1][i][j], d[k-1][i][k] + d[k-1][k][j]);
             }
         }
     }
}

 

幾乎全部介紹動態規劃中最爲著名的「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])。ci

上圖描述了在前面最初試的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算法代碼以下:

1
2
3
4
5
6
void floyd() {
     for ( int k = 1; k <= n; k++)
         for ( int i = 1; i <= n; i++)
             for ( int j = 1; j <= n; j++)
                 d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

所以,經過這篇文章的分析,咱們能夠發現,Floyd算法的的確確是一種典型的動態規劃算法;理解Floyd算法,也能夠幫助咱們進一步理解動態規劃思想。

轉載:http://www.cnblogs.com/chenying99/p/3932877.html

相關文章
相關標籤/搜索