動態規劃演算法學習總結(帶案例)
【動規演算法學習總結】
首先,遇到動態規劃問題要找到三個重要元素: 1.最優子結構 2.邊界 3.狀態轉移方程
【最優子結構】 通俗來說,就是具有規律性的結果的獲取方式。 如上樓梯問題中, 上第10層的情況種類 = 上第8、9層的情況種類之和。第9層的結果又為第7、8層結果之和。 又如擊鼓傳花問題中。 傳m次傳給1的情況種類 = 傳m-1次傳給n、2的情況種類之和。 傳m-1次傳給2的情況又為傳m-2次傳給1、3情況之和。 (詳細內容見另外兩篇部落格)
【邊界】 任何問題都要有邊界,否則我們就要無休止的迴圈下去了。 在得出最優子結構後,在將結果遞迴至某一處時(一般在資料集邊緣),可以直接得出其結果。而這個就是邊界。 如上樓梯問題(初始位置為第0階樓梯),其邊界為:上第1階時,結果為1。 上第二階時,結果為2({1,1},{2}})。 又如擊鼓傳花問題,傳1次傳給2的結果為1,傳1次傳給n的結果為1。
【狀態轉移方程】 在得出最優子結構後,就可以對應歸納出狀態轉移方程。 如上樓梯問題,F(n) = F(n-1) + F(n-2) 但是我個人覺得,如果時間緊迫(如筆試題),也可以放棄得到其最優子結構和最優的狀態轉移方程。 而是採用判斷的形式,得出對應的結果。 如在擊鼓傳花問題當中,我將其分為三中情況:
* dp[m][1]等於第m-1次傳遞到小賽左右兩邊的人的情況之和,即 dp[m][1] = dp[m-1][n] + dp[m-1][2]
* i代表傳遞i次,j代表傳遞給第j個人,則
* j == 1時:
* dp[i][j] = dp[i-1][n] + dp[i-1 ][j+1];
* j == n時:
* dp[i][j] = dp[i-1][j-1] + dp[i-1][1];
* 正常情況:
* dp[i][j] = dp[i-1][j-1] + dp[i-1][j+1];
而對應的程式碼,同樣是判斷
//dynamic planing
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if(j == 1){
dp[i][j] = dp[i-1 ][n] + dp[i-1][j+1];
}else if(j == n){
dp[i][j] = dp[i-1][j-1] + dp[i-1][1];
}else{
dp[i][j] = dp[i-1][j-1] + dp[i-1][j+1];
}
}
}
System.out.println(dp[m][1]);
其次,在得到三個元素後,需要將思路逆轉過來 為什麼說逆轉過來呢? 因為剛才我們得到的最優子結構,是採用自頂向下的思路推理分析的。而我們所採用的動態規劃解法,則自底而上,從我們找到的邊界值開始,從底層,採用for迴圈,一部一部得到上方的值,最終得到我們最頂端的值。 舉例說明: 上樓梯問題,知道1階和2階的結果,我們就可以得到3階的結果。 知道2階和3階的結果,我們就可以得到4階的結果。 …….最終,我們得到了10階的結果。 具體程式碼如下:
static int step(int n){
if (n == 1){return 0; }
if (n == 2){return 1; }
if (n == 3){return 2; }
int a = 1, b = 2, step = 0;
for (int i = 4; i <= n; i++){
step = a + b;
a = b;
b = step;
}
return step;
}
擊鼓傳花問題,我們知道dp[1][2]=1,dp[1][n]=1,其餘dp[1][i]=0。 我們可以根據dp[1][2]=1,dp[1][n] = 1 ,得到dp[2][1] = 2; 根據dp[1][1]=0,dp[1][3] = 0,得到dp[2][2] = 0; 根據dp[1][2] = 1, dp[1][4] = 0,得到 dp[2][3] = 1; …… 最終得到dp[m][1]的結果。
這就是自己學習動態規劃的一些心得。
【補充內容】 其實,在得到【最優子結構】,【邊界】,【狀態轉移方程】後最直接的解法是: 採用遞迴演算法(時間複雜度高,空間複雜度高) 以及備忘錄演算法(時間複雜度低,空間複雜度較高) 最優的才是動態規劃演算法
簡要介紹一下這裡提到的兩種演算法: 【遞迴演算法】 以上樓梯問題為例:
int step(int n){
if(n == 0)
return 0;
if(n == 1)
return 1;
if(n == 2)
return 2;
return step(n-1) + step(n-2);
}
遞迴最為簡單,直接把邊界值,狀態轉移方程帶入求解即可。 但是效率極低,因為其它重複計算了很多內容。(一旦n值極大,程式馬上崩潰)。
【備忘錄演算法】 思路:採用遞迴演算法,但是增加一個緩衝池,每次取值時, 判斷緩衝池中是否有?取出:計算得出並放入緩衝池 虛擬碼如下:
Map<Integer,Integer> cache = new HashMap<>();
int step(int n){
if(n == 0)
return 0;
if(n == 1)
return 1;
if(n == 2)
return 2;
if(cache.contains(n){
return map.get(n)
}else{
int res = step(n-1) + step(n-2);
cache.put(n,res);
return res;
}
}