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


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]=0i∈[1,n]

這樣我們就可以編寫出最為初步的Floyd算法代碼:

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])。

上圖描述了在前面最初試的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://tech.artyoo.me/?p=81


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM