狀態轉移方程
動態規划中當前的狀態往往依賴於前一階段的狀態和前一階段的決策結果。例如我們知道了第i個階段的狀態Si以及決策Ui,那么第i+1階段的狀態Si+1也就確定了。所以解決動態規划問題的關鍵就是確定狀態轉移方程,一旦狀態轉移方程確定了,那么我們就可以根據方程式進行編碼。
在前面的文章《動態規划-開篇》講到了如何設計一個動態規划算法,有以下四個步驟:
1、刻畫一個最優解的結構特征。
2、遞歸地定義最優解的值。
3、計算最優解的值,通常采用自底向上的方法。
4、利用計算出的信息構造一個最優解。
對於確定狀態轉移方程就在第一步和第二步中,首先要確定問題的決策對象,接着對決策對象划分階段並確定各個階段的狀態變量,最后建立各階段的狀態變量的轉移方程。
例如用dp[i]表示以序列中第i個數字結尾的最長遞增子序列長度和最長公共子序列中用dp[i][j]表示的兩個字符串中前 i、 j 個字符的最長公共子序列,我們就是通過對這兩個數字量的不斷求解最終得到答案的。這個數字量就被我們稱為狀態。狀態是描述問題當前狀況的一個數字量。首先,它是數字的,是可以被抽象出來保存在內存中的。其次,它可以完全的表示一個狀態的特征,而不需要其他任何的輔助信息。最后,也是狀態最重要的特點,狀態間的轉移完全依賴於各個狀態本身,如最長遞增子序列中,dp[x]的值由 dp[i](i < x)的值確定。若我們在分析動態規划問題的時候能夠找到這樣一個符合以上所有條件的狀態,那么多半這個問題是可以被正確解出的。所以說,解動態規划問題的關鍵,就是尋找一個好的狀態。
總結
下面對這幾天的學習總結一下,將我遇到的各種模型的狀態轉移方程匯總如下:
1、最長公共子串
假設兩個字符串為str1和str2,它們的長度分別為n和m。d[i][j]表示str1中前i個字符與str2中前j個字符分別組成的兩個前綴字符串的最長公共長度。這樣就把長度為n的str1和長度為m的str2划分成長度為i和長度為j的子問題進行求解。狀態轉移方程如下:
- dp[0][j] = 0; (0<=j<=m)
- dp[i][0] = 0; (0<=i<=n)
- dp[i][j] = dp[i-1][j-1] +1; (str1[i] == str2[j])
- dp[i][j] = 0; (str1[i] != str2[j])
因為最長公共子串要求必須在原串中是連續的,所以一但某處出現不匹配的情況,此處的值就重置為0。
詳細代碼請看最長公共子串。
2、最長公共子序列
區分一下,最長公共子序列不同於最長公共子串,序列是保持子序列字符串的下標在str1和str2中的下標順序是遞增的,該字符串在原串中並不一定是連續的。同樣的我們可以假設dp[i][j]表示為字符串str1的前i個字符和字符串str2的前j個字符的最長公共子序列的長度。狀態轉移方程如下:
- dp[0][j] = 0; (0<=j<=m)
- dp[i][0] = 0; (0<=i<=n)
- dp[i][j] = dp[i-1][j-1] +1; (str1[i-1] == str2[j-1])
- dp[i][j] = max{dp[i][j-1],dp[i-1][j]}; (str1[i-1] != str2[j-1])
詳細代碼請看最長公共子序列。
3、最長遞增子序列(最長遞減子序列)
因為兩者的思路都是一樣的,所以只給出最長遞增子序列的狀態轉移方程。假設有序列{a1,a2,...,an},我們求其最長遞增子序列長度。按照遞推求解的思想,我們用F[i]代表若遞增子序列以ai結束時它的最長長度。當 i 較小,我們容易直接得出其值,如 F[1] = 1。那么,如何由已經求得的 F[i]值推得后面的值呢?假設,F[1]到F[x-1]的值都已經確定,注意到,以ax 結尾的遞增子序列,除了長度為1的情況,其它情況中,ax都是緊跟在一個由 ai(i < x)組成遞增子序列之后。要求以ax結尾的最長遞增子序列長度,我們依次比較 ax 與其之前所有的 ai(i < x), 若ai小於 ax,則說明ax可以跟在以ai結尾的遞增子序列之后,形成一個新的遞 增子序列。又因為以ai結尾的遞增子序列最長長度已經求得,那么在這種情況下,由以 ai 結尾的最長遞增子序列再加上 ax 得到的新的序列,其長度也可以確定,取所有這些長度的最大值,我們即能得到 F[x]的值。特殊的,當沒有ai(i < x)小 於ax, 那么以 ax 結尾的遞增子序列最長長度為1。 即F[x] = max{1,F[i]+1|ai<ax && i<x}。
詳細代碼請看最長遞增子序列。
4、最大子序列和的問題
假設有序列{a1,a2,...,an},求子序列的和最大問題,我們用dp[i]表示以ai結尾的子序列的最大和。
dp[1] = a1; (a1>=0 && i == 1)
dp[i] = dp[i-1]+ai; (ai>=0 && i>=2)
dp[i] = 0; (dp[i-1] + ai <=0 && i>=2)
詳細代碼請看最大子序列的和。
5、數塔問題(動態搜索)
給定一個數組data[n][m]構成一個數塔求從最上面走到最低端經過的路徑和最大。可以假設dp[i][j]表示走到第i行第j列位置處的最大值,那么可以推出狀態轉移方程:
dp[i][j] = max{dp[i-1][j-1],dp[i-1][j]} + data[i][j];

for(i=n-1;i>=1;i--){ for(j=1;j<=i;j++){ dp[i][j]=max{dp[i-1][j-1],dp[i-1][j]}+s[i][j] } }
6、(01)背包問題
這是一個經典的動態規划問題,另外在貪心算法里也有背包問題,至於二者的區別在此就不做介紹了。
假設有N件物品和一個容量為V的背包。第i件物品的體積是v[i],價值是c[i],將哪些物品裝入背包可使價值總和最大?
每一種物品都有兩種可能即放入背包或者不放入背包。可以用dp[i][j]表示第i件物品放入容量為j的背包所得的最大價值,則狀態轉移方程可以推出如下:
dp[i][j]=max{dp[i-1][j-v[i]]+c[i],dp[i-1][j]};

for (int i = 1;i <= N;i++) //枚舉物品 { for (int j = 0;j <= V;j++) //枚舉背包容量 { f[i][j] = f[i - 1][j]; if (j >= v[i]) { f[i][j] = Max(f[i - 1][j],f[i - 1][j - v[i]] + c[i]); } } }
可以參照動態規划 - 0-1背包問題的算法優化、動態規划-完全背包問題、動態規划-多重背包問題
7、矩陣連乘(矩陣鏈問題)-參考《算法導論》
例如矩陣鏈<A1,A2,A3>,它們的維數分別為10*100,100*5,5*50,那么如果順序相乘即((A1A2)A3),共需10*100*5 + 10*5*50 = 7500次乘法,如果按照(A1(A2A3))順序相乘,卻需做100*5*50 + 10*100*50 = 75000次乘法。兩者之間相差了10倍,所以說矩陣鏈的相乘順序也決定了計算量的大小。
我們用利用動態規划的方式(dp[i][j]表示第i個矩陣至第j個矩陣這段的最優解,還有對於兩個矩陣A(i,j)*B(j,k)則需要i*j*k次乘法),推出狀態轉移方程:
dp[i][j] = 0; (i ==j,表示只有一個矩陣,計算次數為0)
dp[i][j] = min{dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j]}; (i<j && i<=k<j)
dp[1][n]即為最終求解.

#define MAXSIZE 100 int dp[MAXSIZE][MAXSIZE];//存儲最小的就算次數 int s[MAXSIZE][MAXSIZE];//存儲斷點,用在輸出上面 int i, j, tmp; for (int l = 2; l <= n; l++){//j-i的長度,由於長度為1是相同的矩陣那么為0不用計算 for (i = 1; i <= n - l + 1; i++){//由於j-i =l - 1 , 那么j的最大值為n,所以i上限為 n - l+1; j = i + l - 1;//由於j-i = l - 1 , 那么j = l+i-1 dp[i][j] = dp[i + 1][j] + r[i] * c[i] * c[j];//初始化,就是k = i; s[i][j] = i; for (k = i + 1; k < j; k++){//循環枚舉k i < k < j tmp = dp[i][k] + dp[k + 1][j] + r[i] * c[k] * c[j]; if (dp[i][j] > tmp){ dp[i][j] = tmp;//更新為最小值 s[i][j] = k; } } } } //遞歸調用輸出 void output(int i, int j){ if (i == j){ printf("A%d", i);//當兩個相等的時候就不用繼續遞歸就輸出A return;//返回上一層 } else{ printf("("); output(i, s[i][j]); printf(" x "); output(s[i][j] + 1, j); printf(")"); } }
ps
如有錯誤的地方或者本人理解錯的地方,請指出,共同進步!!!