# 動態規劃 :::info 遞迴是由大到小,動態規劃是由小到大:把大問題拆成小問題,記住小問題的答案,組合成大問題答案 ::: > 動態規劃不是遞迴,不會要return自己。 > 動態規劃簡單說就是以空間複雜度為代價,換取時間複雜度的優化。 # 1D DP DP的題目大多符合下列步驟 例: **爬樓梯問題** > 有一棟樓梯有 n 階,每次你可以爬 1 階或 2 階,問你有幾種不同的爬法? > 要素 | 說明 | 爬樓梯範例 | -------- | -------- | -------- | 1️⃣ 狀態定義 | 記錄「子問題」的解 | dp[i] = 爬到第 i 階的方法數 2️⃣ 狀態轉移方程 | 如何用小問題解大問題 | dp[i] = dp[i-1] + dp[i-2] 3️⃣ 初始狀態 | base case | dp[1] = 1, dp[2] = 2 4️⃣ 遍歷順序 | 從小到大累積結果 | for i in range(3, n+1) ## 外觀數列(費波納契數列) ```cpp= ll fib(ll cnt){ if (cnt == 0) return 0; vector<ll> li={1,1}; for (ll i=2; i<=cnt; i++){ li.push_back(li[i-1]+li[i-2]); } return li[cnt]; } int main(){ io; ll a; while (cin >> a) { cout << fib(a) << endl; } } ``` > 通常會建議用遞迴寫費波納契數列,網路上找的到很多範例,這裡就不特別做說明了 # 2D DP 考慮以下問題: LCS(Longest Common Subsequence)最長共同子序列 [UVa - LCS](https://zerojudge.tw/ShowProblem?problemid=a133) 大家的想法通常會是:用雙重迴圈去跑,只要一A[i]與B[j]一樣就+1。 但如果今天題目輸入的兩個字串AB都超長,此時就會爆炸,這時不妨試一下DP的寫法! DP LCS的想法是,我一直去比較1、2、3:怎樣才是最長序列(最優解) ![ABCBDAB (1)](https://hackmd.io/_uploads/ry5986e1Zx.gif) # 滾動 DP > DP 的技巧有許多種,通稱為 DP 優化,每種學問都很深,可以有效地減少時間複雜 度。我們先來看一種減少空間複雜度的技巧:滾動 DP。 **既然每次計算的時候只需要前兩項,那剩下那些用不到的就可以丟掉了。** ```python= MOD = 10**9 + 7 def fibonacci(n): a, b = 0, 1 for _ in range(2, n + 1): a, b = b, (a + b) % MOD return b if n > 0 else 0 n = int(input()) print(fibonacci(n)) ``` # 組合總和問題 ## 演練 - 1 ![image](https://hackmd.io/_uploads/r1DGt_P1lx.png) * 定義轉移: dp[i] = 骰出點數為 i 的組合數 * 定義完轉移式之後接下來就可以開始把公式推出來了 • 對於點數 i 來說,要使骰出的點數總和為 i ,會有 6 種可能 • 點數 i - 6 時再骰出 1 個 6 點 • 點數 i - 5 時再骰出 1 個 5 點 • 點數 i - 4 時再骰出 1 個 4 點 • and so on… 因此轉移式為 dp[i] = dp[i-1] + dp[i-2] + … + dp[i-6] > 其實就是加法原理 ### C++ domjudge python 一直超時idk why ```python= #include <bits/stdc++.h> #define io ios::sync_with_stdio(0);cin.tie(0);cout.tie(0); using namespace std; typedef long long ll; int main(){ io; ll i,j, n, MOD = pow(10,9)+7; cin >> n; ll dp[n+1] = {0}; dp[0] = 1; // 直接丟骰子就可以實現 for (i=1 ; i<n+1; i++){ for (j=1 ; j<7; j++){ if (i-j >= 0){ dp[i] += dp[i-j]; dp[i] %= MOD; } } } cout << dp[n]; } ``` 過程演示 ```python= dp[0] = 1 # base dp[1] = dp[0] = 1 dp[2] = dp[1] + dp[0] = 1 + 1 = 2 dp[3] = dp[2] + dp[1] + dp[0] = 2 + 1 + 1 = 4 dp[4] = dp[3] + dp[2] + dp[1] + dp[0] = 4 + 2 + 1 + 1 = 8 dp[5] = dp[4] + dp[3] + dp[2] + dp[1] + dp[0] = 8 + 4 + 2 + 1 + 1 = 16 dp[6] = dp[5] + dp[4] + dp[3] + dp[2] + dp[1] + dp[0] = 16 + 8 + 4 + 2 + 1 + 1 = 32 dp[7] = dp[6] + dp[5] + dp[4] + dp[3] + dp[2] + dp[1] = 32 + 16 + 8 + 4 + 2 + 1 = **63** ``` 你會發現到DP[7]就不把dp[0]加進來了 :::warning 因為骰子沒辦法直接一丟就丟出7 ::: > dp[1] 是指全部1的情況:1+1+1+1+... # 演練 - 2 ![image](https://hackmd.io/_uploads/ryO4TR_1xl.png) 一樣,用上面的動態規劃,加法原理求解就好 * 設 dp[i]為湊出金額 i 所需的最少硬幣數量。 * 初始化:dp[0] = 0(湊出金額 0,不用硬幣) ```cpp= int main(){ io; ll i,j, n, target; cin >> n >> target; vector<int> li(n); for (i=0;i<n;i++) cin >> li[i]; ll INT = INT_MAX; vector<ll> dp(target+1, INT); dp[0] = 0; // 直接丟骰子就可以實現 for (i=1 ; i<target+1; i++){ for (auto j : li){ if (i-j >= 0){ dp[i] = min(dp[i-j]+1,dp[i]); } } } cout << dp[target]; } ``` 以範例1做舉例: > dp[11] = min(dp[11], dp[11 - 5] + 1) > = min(dp[11], dp[6] + 1) 意思是: :::warning 如果我知道湊出 6 元最少需要幾枚硬幣,那我只要再加上一枚 5 元硬幣,就可以湊出 11 元了! ::: 所以: dp[6] = 2(例如可能是 1+5) 那 dp[11] = dp[6] + 1 = 3 也就是:加上這個 +1,代表你用了這個 coin! ## 進階 [LC39](https://hackmd.io/@QlAoGV8nSFyJy-hYETkq-g/Sy1HwzZhke/%2FuN65Ti31REesr868Sx9auw) 這題不是要問你有幾種組合,而是要你把所有組合都列出來 (當然,題目測資會比較小) # DP - 枚舉 > 枚舉就是一種暴力解,但可以透過一些邊界,條件的判斷來**剪枝** --- :::info 趁機宣傳一下我自己的個人網站跟Youtube頻道 !! **[個人網站](https://hyc.eshachem.com/) | [Youtube頻道](https://www.youtube.com/@Hy.C)** ::: @2025 Hy.C 陳毓 > Copyright ©Hy.C 陳毓 CC BY-NC-SA 4.0 | 禁止商業用途 | 轉載標記出處 | 改編作品必須在相同條款下分享。