Semorphe

動態規劃

把算過的答案記下來,不要算第二次。 · ⏱ 約 40 分鐘

你會學到三件事

  1. DP 的兩件事:狀態和轉移
  2. 為什麼遞迴費氏數會慢到跑不完
  3. 邊界值就是「不需要推導的那幾格」

開始之前

費氏數:f(n) = f(n-1) + f(n-2)。

用C++ 入門第 16 課的遞迴寫,f(50) 跑不完。

🟢 先把編輯器打開,跟著下面一起做:跟著做

一、為什麼慢

            f(5)
          /      \
      f(4)        f(3)
     /    \      /    \
  f(3)   f(2)  f(2)  f(1)
  /  \
f(2) f(1)          ← 🔴 f(3) 算了兩次、f(2) 算了三次

同一個東西被重複算了無數次,f(50) 大約要算 10¹⁰ 次。

而其實只有 51 個不同的答案。

二、記下來

算過的就記起來,不要再算第二次:

1vector<int> dp(n + 1);2dp[0] = 0;3dp[1] = 1;4for (int i = 2; i <= n; i++) {5    dp[i] = dp[i - 1] + dp[i - 2];6}
變數n1+00112變數n變數i變數dp變數i1-陣列的第 [] 格變數dp變數i2-陣列的第 [] 格+設定 陣列dp的第 [] 格 ←計數i從到(含)重複設定 陣列dp的第 [] 格 ←設定 陣列dp的第 [] 格 ←建立int(整數)列表dp大小每格填複製自使用命名空間std引入函式庫vector(動態列表)12345

從小算到大,每個只算一次。O(n),f(50) 瞬間。

三、DP 就是兩件事

這一題是什麼
狀態 dp[i] = 第 i 個費氏數
轉移 dp[i] = dp[i-1] + dp[i-2]
邊界 dp[0] = 0、dp[1] = 1

想 DP 的時候,第一句話永遠是「dp[i] 代表什麼」。

講不清楚 dp[i] 是什麼,後面的轉移一定寫不對—— 而且它會寫出一個「看起來對、樣例也過」的東西。

⚠️ 邊界是「不需要推導的那幾格」。dp[1] 沒有 dp[-1] 可以用, 所以它必須直接給。少給一格,整條鏈就會從錯的地方開始。

四、它和分治的差別

子問題
分治(第 9 課) 各自獨立,不重疊
DP 會重疊——所以才要記

判準:畫出遞迴樹,有沒有同一個節點出現兩次?有 → DP,沒有 → 分治。

五、⚠️ 兩個常見的坑

一、溢位(第 1 課)。費氏數第 47 個就超過 int。 DP 的答案常常很大,用 long long。

二、記憶體。dp[10000][10000] 是 10⁸ 個 int = 400 MB,爆掉。

而多半只需要上一列:

1// dp[i][j] 只用到 dp[i-1][...] 的話2// 兩列輪流用就夠了 —— 這叫「滾動陣列」
備註:兩列輪流用就夠了 —— 這叫「滾動陣列」備註:dp[i][j] 只用到 dp[i-1][...] 的話12

六、常見的幾類

題型 狀態長什麼樣
費氏 / 爬樓梯 dp[i] = 到第 i 階有幾種走法
0/1 背包 dp[i][w] = 前 i 個物品、容量 w 的最大價值
最長遞增子序列 dp[i] = 以第 i 個結尾的最長長度
編輯距離 dp[i][j] = 前 i 個字變成前 j 個字要幾步

它們的差別全在「dp[i] 是什麼」那一句話上。

完成的樣子

這一課的參考做法——程式碼每一行的號碼,就印在對應的那塊積木上。

1#include <iostream>2#include <vector>3using namespace std;4int main() {5    int n = 10;6    vector<int> dp(n + 1);7    dp[0] = 0;8    dp[1] = 1;9    for (int i = 2; i <= n; i++) {10        dp[i] = dp[i - 1] + dp[i - 2];11    }12    cout << dp[n] << endl;13    return 0;14}
10變數n1+00112變數n變數i變數dp變數i1-陣列的第 [] 格變數dp變數i2-陣列的第 [] 格+設定 陣列dp的第 [] 格 ←變數dp變數n陣列的第 [] 格換行0回傳印出計數i從到(含)重複設定 陣列dp的第 [] 格 ←設定 陣列dp的第 [] 格 ←建立int(整數)列表dp大小每格填複製自宣告int(整數)變數n=定義函式 回傳型別int(整數)main使用命名空間std引入函式庫vector(動態列表)引入函式庫iostream(輸入輸出)123456789101213

換你了

爬樓梯:一次可以爬 1 階或 2 階,n 階有幾種走法?

⚠️ 先寫出這一句:「dp[i] = ______」。寫不出來就先不要寫程式。

(答案是費氏數。而重點是你自己推出那個轉移,不是那個答案。)

到編輯器做這一題:練習:爬樓梯——n 階有幾種走法

做一個

-2 1 -3 4 -1 2 1 -5 4,找出連續一段加起來最大是多少。

🔴 狀態是「以第 i 格結尾的最大和」,而轉移只有一個選擇: 接上前面那一段,還是從這裡重新開始。 ⚠️ 試遍所有起點終點是 O(n²),而這個做法是一趟。

到編輯器做這一題:做一個:最大連續子陣列和

這一課你做了什麼

如果卡住了

你看到 多半是因為
前幾項就錯 邊界沒給對
大的 n 答案變負 溢位。用 long long
記憶體爆掉 二維太大。考慮滾動陣列
轉移式寫不出來 「dp[i] 是什麼」還沒講清楚
在編輯器打開這一課 →