先看幾類數字三角形的問題,經過對這幾個問題的分析來理解有關動態規劃的基本思想數組
數字三角形Iapp
問題描述:spa
有一個由正整數組成的三角形,第一行只有一個數,除了最下行以外 每一個數的左下方和右下方各有一個數,從第一行的數開始,每次能夠往左下或右下走一格,直到走到三角形底端,把沿途通過的數所有加起來做爲得分。如何走,使得這個得分儘可能大?code
分析:blog
如何走,是一個決策問題,很容易聯想到一種貪心策略是:每次選數字大的那個方向走。然而很明顯,這種決策方案是錯誤的。由於若是按這種方案,獲得的結果是1→3→10→3,總和爲17,而正確的答案是:1→2→1→20,總和爲24,這種方案錯在了「目光短線「。遞歸
再次分析,賦予d[i,j]的含義爲以(i,j)爲首的三角形的最大和,那麼這個問題就轉換成了求d(1,1)。很明顯,d[i,j]=a[i,j]+max(d[i+1,j],d[i+1,j+1])。這裏體現的一個思想就是:將大問題分解爲小問題,由小問題的求解來獲得大問題的答案。class
這個思路是正確的,那麼接下來要考慮的一個問題就是如何計算了。有三種方法,遞歸、遞推以及記憶化搜索,接下來就分析這三種計算方法的具體作法。基礎
方法一:遞歸擴展
int solve(int i, int j) { if(i == n) return a[i][j]; else return a[i][j] + max(solve(i + 1, j), solve(i + 1, j + 1)); }
這個方法存在沒必要要的重複計算,低效搜索
方法二:遞推
void solve() { for(int j = 1; j <= n; j++) dp[n][j] = a[n][j]; for(int i = n - 1; i >= 1; i--) for(int j = 1; j <= i; j++) dp[i][j] = a[i][j] + max(d[i + 1][j], dp[i + 1][j + 1]); }
遞推自底向上,避免了重複計算,時間複雜度爲O(n2)
方法三:記憶化搜索
int solve(int i, int j) { //首先初始化dp爲-1 memset(dp,-1,sizeof(dp)); if(i == n) return a[i][j]; if(dp[i][j] >= 0) return dp[i][j]; dp[i][j] = a[i][j] + max(solve(i + 1, j), solve(i + 1, j + 1)); return dp[i][j]; }
這個方法就是在遞歸的基礎上加上了記憶化,一樣避免了重複計算,時間複雜度爲O(n2)
對比這幾種方法,能夠看出重疊子問題(overlapping subproblems) 是動態規劃展現威力的關鍵
經過這個例子,能夠理解動態規劃的基本思想:將大問題分解成小問題,創建子問題的描述,創建狀態間的轉移關係,使用遞推或記憶化搜索來實現。而要使用好動態規劃的幾個關鍵的重點就是:狀態的定義和狀態轉移方程。
數字三角形II
問題描述:
在數字三角形I的基礎上稍微改變了一下。從第一行的數開始,除了某一次能夠走到下一行的任意位置外,每次都只能左下或右下走一格,直到走到最下行,把沿途通過的數所有加起來。如何走,使得這個和儘可能大?
分析:
這題的關鍵在於後效性問題,即先前的決策可能影響後續的決策,要消除後效性,就得拓展狀態定義(加限制條件)來將有後效性的部分包含進去。
1 memset (maxx, 0, sizeof (maxx)); 2 for(j = 1; j <= n; j ++) 3 { 4 d[n][j][1] = d[n][j][0] = a[n][j]; 5 if(a[n][j] > max[n]) 6 maxx[n] = a[n][j]; 7 } 8 for(i = n - 1; i >= 1; i--) 9 { 10 for(j = 1; j <= i; j++) 11 { 12 d[i][j][0] = a[i][j] + max(d[i + 1][j][0], d[i + 1][j + 1][0]); 13 if(d[i][j][0] > maxx[i]) 14 maxx[i] = d[i][j][0]; 15 d[i][j][1] = a[i][j] + max(d[i + 1][j][1], d[i + 1][j + 1][1], maxx[i + 1]); 16 } 17 }
數字三角形III
問題描述:
在數字三角形I的基礎上,問題變成了,如何走,使得這個和的個位數儘可能大?
分析:
符合無後效性,可是卻出現了另外一個問題,那就是由子問題的最優解不能推出全局的最優解。好比看下面一個例子
對於灰色格子(2,1)來講,根據狀態定義,d[2,1]=6(從 此格子出發路徑上數之和的最大個位數),d[2,2]=0(不管怎麼走,個位數都是0), 根據前面的「遞推方程」算出d[1,1]應是1,但實際上d[1,1]等於9。問題出在:全局最優解5-0-4-0並無包含子問題最優解0-4-2,即不知足最優子結構。不知足最優子結構的狀況一般也能夠考慮擴展狀態定義。
1 for(j = 1; j <= n; j++) 2 d[n][j][a[n][j] % 10] = true; 3 for(i = n - 1; i >= 1; i--) 4 { 5 for(j = 1; j <= i; j++) 6 { 7 for(k = 0; k <= 9; k++) 8 { 9 d[i][j][k] = false; 10 t = (10 - a[i][j]) % 10; 11 if(d[i + 1][j][t] || d[i + 1][j + 1][t]) 12 d[i][j][k] = true; 13 } 14 } 15 }
總結:
動態規劃能夠解決的某類問題:
每一個階段的最優狀態能夠從以前某個階段的某個或某些狀態直接獲得而無論以前這個狀態是如何獲得的。
其中,每一個階段的最優狀態能夠從以前某個階段的某個或某些狀態直接獲得,這個性質叫作最優子結構;而無論以前這個狀態是如何獲得的,這個性質叫作無後效性。