1. 程式人生 > 實用技巧 >動態規劃學習筆記

動態規劃學習筆記

動態規劃

概念

維基:

動態規劃(英語:Dynamic programming,簡稱DP)是一種在數學、管理科學、電腦科學、經濟學和生物資訊學中使用的,通過把原問題分解為相對簡單的子問題的方式求解複雜問題的方法。
動態規劃常常適用於有重疊子問題[1]和最優子結構性質的問題,動態規劃方法所耗時間往往遠少於樸素解法。

優勢

動態規劃在查詢有很多重疊子問題的情況的最優解時有效。它將問題重新組合成子問題。為了避免多次解決這些子問題,它們的結果都逐漸被計算並被儲存,從簡單的問題直到整個問題都被解決。因此,動態規劃儲存遞迴時的結果,因而不會在解決同樣的問題時花費時間。

適用情況

  1. 最優子結構性質。如果問題的最優解所包含的子問題的解也是最優的,我們就稱該問題具有最優子結構性質(即滿足最優化原理)。最優子結構性質為動態規劃演算法解決問題提供了重要線索。
  2. 無後效性。即子問題的解一旦確定,就不再改變,不受在這之後、包含它的更大的問題的求解決策影響。
  3. 子問題重疊性質。子問題重疊性質是指在用遞迴演算法自頂向下對問題進行求解時,每次產生的子問題並不總是新問題,有些子問題會被重複計算多次。動態規劃演算法正是利用了這種子問題的重疊性質,對每一個子問題只計算一次,然後將其計算結果儲存在一個表格中,當再次需要計算已經計算過的子問題時,只是在表格中簡單地檢視一下結果,從而獲得較高的效率,降低了時間複雜度。

核心

  • 狀態轉移方程
  • 備忘錄(DP Table)

狀態轉移方程思維框架

來自labuladong

# 初始化 base case
dp[0][0][...] = base
# 進行狀態轉移
for 狀態1 in 狀態1的所有取值:
    for 狀態2 in 狀態2的所有取值:
        for ...
            dp[狀態1][狀態2][...] = 求最值(選擇1,選擇2...)

經典問題

湊零錢問題

給你 k 種面值的硬幣,面值分別為 c1, c2 ... ck,每種硬幣的數量無限,再給一個總金額 amount,問你最少需要幾枚硬幣湊出這個金額,如果不可能湊出,演算法返回 -1 。
解1

def coinChange(coins: List[int], amount: int):
    # 備忘錄
    memo = dict()
    def dp(n):
        # 查備忘錄,避免重複計算
        if n in memo: return memo[n]
        # base case
        if n == 0: return 0
        if n < 0: return -1
        res = float('INF')
        for coin in coins:
            subproblem = dp(n - coin)
            if subproblem == -1: continue
            res = min(res, 1 + subproblem)
        
        # 記入備忘錄
        memo[n] = res if res != float('INF') else -1
        return memo[n]
    
    return dp(amount)

解2

int coinChange(vector<int>& coins, int amount) {
    // 陣列大小為 amount + 1,初始值也為 amount + 1
    vector<int> dp(amount + 1, amount + 1);
    // base case
    dp[0] = 0;
    // 外層 for 迴圈在遍歷所有狀態的所有取值
    for (int i = 0; i < dp.size(); i++) {
        // 內層 for 迴圈在求所有選擇的最小值
        for (int coin : coins) {
            // 子問題無解,跳過
            if (i - coin < 0) continue;
            dp[i] = min(dp[i], 1 + dp[i - coin]);
        }
    }
    return (dp[amount] == amount + 1) ? -1 : dp[amount];
}