《資料結構與演算法之美》28——動態規劃理論
前言
上一節通過兩個經理案例初步認識動態規劃,今天這一節主要講動態規劃的理論知識。
“一個模型三個特徵”理論講解
實際上,動態規劃作為一個非常成熟的演算法思想,這部分理論總結為“一個模型三個特徵”。
一個模型
一個模型指動態規劃適合解決的問題模型。這個模型定義為“多階段決策最優解模型”。
一般是用動態規劃來解決最優問題。而解決問題的過程,需要經歷多個決策階段。每個決策階段都對應著一組狀態。然後尋找一組決策序列,經過這組決策序列,能夠產生最終期望求解的值。
三個特徵
三個特徵分別是:最優子結構、無後效性和重複子問題。
- 最優子結構:問題的最優解包含子問題的最優解。
- 無後效性:有兩層含義。
- 第一層,在推導後面階段的狀態時,只關心前面階段的狀態值。
- 第二層,某階段的狀態一旦確定,就不受之後階段的決策影響。
- 重複子問題:不同的決策序列,到達某個相同的階段時,可能會產生重複的狀態。
兩種動態規劃解題思路總結
解決動態規劃問題,一般有兩種思路。分別是狀態轉移表法和狀態轉移方程法。
狀態轉移表法
狀態轉移表法的解題思路概括為:回溯演算法實現-定義狀態-畫遞迴樹-找重複子問題-畫狀態轉移表-根據遞推關係填表-將填表過程翻譯成程式碼。
我們來看一下,如何套用狀態轉移表法來解決動態規劃問題。
假設我們有一個n乘以n的矩陣w[n][n]。矩陣儲存的都是正整數。棋子起始位置在左上角,終止位置在右下角。那從左上角移動到右下角的最短路徑長度是多少?
回溯演算法實現
public class Solution { private int minDist = int.MaxValue; public int MinDist { get { return minDist; } } // 呼叫方式:MinDistBT(0, 0, 0, w, n); public void MinDistBT (int i, int j, int dist, int[][] w, int n) { // 到達n-1, n-1這個位置了 if (i == n - 1 && j == n - 1) { dist = dist + w[i][j]; if (dist < minDist) minDist = dist; return; } if (i < n - 1) { // 往下走,更新i=i+1, j=j MinDistBT (i + 1, j, dist + w[i][j], w, n); } if (j < n - 1) { // 往右走,更新i=i, j=j+1 MinDistBT (i, j + 1, dist + w[i][j], w, n); } } }
定義狀態
從回溯程式碼的函式呼叫可知,每一個狀態包含三個變數(i, j, dist),其中 i,j 分別表示行和列,dist 表示從起點到達(i, j)的路徑長度。
畫遞迴樹
有了回溯程式碼和狀態定義,把每個狀態作為一個節點,畫出遞迴樹。
找重複子問題
從上圖可知,存在重複子問題。
畫狀態轉移表
我們畫出一個二維狀態表,表中的行、列表示棋子所在的位置,表中的數值表示從起點到這個位置的最短路徑。
根據遞推關係填表
按照決策過程,通過不斷狀態遞推演進,將狀態表填好。
將填表過程翻譯成程式碼
public class Solution2 {
public int MinDistDP (int[][] matrix, int n) {
int[][] states = new int[n][];
for (int i = 0; i < n; i++) {
states[i] = new int[n];
}
int sum = 0;
for (int j = 0; j < n; ++j) { // 初始化states的第一行資料
sum += matrix[0][j];
states[0][j] = sum;
}
sum = 0;
for (int i = 0; i < n; ++i) { // 初始化states的第一列資料
sum += matrix[i][0];
states[i][0] = sum;
}
for (int i = 1; i < n; ++i) {
for (int j = 1; j < n; ++j) {
states[i][j] = matrix[i][j] + Math.Min (states[i][j - 1], states[i - 1][j]);
}
}
return states[n - 1][n - 1];
}
}
狀態轉移方程法
狀態轉移方程法的解題思路概括為:找最優子結構-寫狀態轉移方程-將狀態轉移方程翻譯成程式碼。
還是拿上面的例子來說明。
找最優子結構
min_dist(i, j)可以通過min_dist(i, j-1)和min_dist(i-1, j)兩個狀態推匯出來,符合“最優子結構”。
寫狀態轉移方程
min_dist(i, j) = w[i][j] + min(min_dist(i, j-1), min_dist(i-1, j))
強調一下,狀態轉移方程是解決動態規劃的關鍵。
將狀態轉移方程翻譯成程式碼
一般情況下,有兩種程式碼實現方法:
- 遞迴+“備忘錄”
- 迭代遞推
用遞迴+“備忘錄”將狀態轉移方程翻譯成程式碼。
public class Solution3 {
private int[, ] matrix = new int[4, 4] { { 1, 3, 5, 9 }, { 2, 1, 3, 4 }, { 5, 2, 6, 7 }, { 6, 8, 4, 3 } };
private int n = 4;
private int[, ] mem = new int[4, 4];
public int MinDist (int i, int j) { // 呼叫MinDist(n-1, n-1)
if (i == 0 && j == 0) return matrix[0, 0];
if (mem[i, j] > 0) return mem[i, j];
int minLeft = int.MaxValue;
if (j - 1 >= 0) {
minLeft = MinDist (i, j - 1);
}
int minUp = int.MaxValue;
if (i - 1 >= 0) {
minUp = MinDist (i - 1, j);
}
int curMinDist = matrix[i, j] + Math.Min (minLeft, minUp);
mem[i, j] = curMinDist;
return curMinDist;
}
}
總結
動態規劃有兩種解題思路:狀態轉移表法和狀態轉移方程法。
狀態轉移表法的解題思路概括為:回溯演算法實現-定義狀態-畫遞迴樹-找重複子問題-畫狀態轉移表-根據遞推關係填表-將填表過程翻譯成程式碼。
狀態轉移方程法的解題思路概括為:找最優子結構-寫狀態轉移方程-將狀態轉移方程翻譯成程式碼。