---
# System prepended metadata

title: 動態規劃

---

# 動態規劃
:::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 | 禁止商業用途 | 轉載標記出處 | 改編作品必須在相同條款下分享。