1. 程式人生 > 實用技巧 >《資料結構與演算法之美》28——動態規劃理論

《資料結構與演算法之美》28——動態規劃理論

前言

上一節通過兩個經理案例初步認識動態規劃,今天這一節主要講動態規劃的理論知識。

“一個模型三個特徵”理論講解

實際上,動態規劃作為一個非常成熟的演算法思想,這部分理論總結為“一個模型三個特徵”。

一個模型

一個模型指動態規劃適合解決的問題模型。這個模型定義為“多階段決策最優解模型”。

一般是用動態規劃來解決最優問題。而解決問題的過程,需要經歷多個決策階段。每個決策階段都對應著一組狀態。然後尋找一組決策序列,經過這組決策序列,能夠產生最終期望求解的值。

三個特徵

三個特徵分別是:最優子結構無後效性重複子問題

  • 最優子結構:問題的最優解包含子問題的最優解。
  • 無後效性:有兩層含義。
    1. 第一層,在推導後面階段的狀態時,只關心前面階段的狀態值。
    2. 第二層,某階段的狀態一旦確定,就不受之後階段的決策影響。
  • 重複子問題:不同的決策序列,到達某個相同的階段時,可能會產生重複的狀態。

兩種動態規劃解題思路總結

解決動態規劃問題,一般有兩種思路。分別是狀態轉移表法狀態轉移方程法

狀態轉移表法

狀態轉移表法的解題思路概括為:回溯演算法實現-定義狀態-畫遞迴樹-找重複子問題-畫狀態轉移表-根據遞推關係填表-將填表過程翻譯成程式碼

我們來看一下,如何套用狀態轉移表法來解決動態規劃問題。

假設我們有一個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;
    }
}

總結

動態規劃有兩種解題思路:狀態轉移表法和狀態轉移方程法。

狀態轉移表法的解題思路概括為:回溯演算法實現-定義狀態-畫遞迴樹-找重複子問題-畫狀態轉移表-根據遞推關係填表-將填表過程翻譯成程式碼

狀態轉移方程法的解題思路概括為:找最優子結構-寫狀態轉移方程-將狀態轉移方程翻譯成程式碼