---
tags: 進階班
---
# DP 動態規劃
## 主要觀念
- 把大問題拆分成小問題
- 把會算到很多遍的儲存下來
<!--
我把題目大概補了上來
有些不知道怎麼解釋:poop:
-->
## 費氏數列
### 遞迴解
```cpp=
int fib(int n) {
if(n == 1 || n == 0) return 1;
else return fib(n - 1) + fib(n - 2);
}
```
**<center>遞迴的過程</center>**
```graphviz
digraph main{
f_0[label = "fib(0)"]
f_1[label = "fib(1)"]
f_1_1[label = "fib(1)"]
f_2[label = "fib(2)"]
"fib(4)" -> {"fib(3)", "fib(2)"}
"fib(3)" -> {f_2, f_1_1}
"f_2" -> {f_0, f_1}
"fib(2)" -> {"fib(0)", "fib(1)"}
}
```
**時間複雜度$O(\phi^n)$**
---
### DP解
可以用一個陣列來記錄`fib(n)`的值,如果有重複算到的就直接回傳即可。
```cpp=
const int mxN = 10//n 最大的值
int dp[mxN]; //dp(大小, 初始值)
dp[0] = dp[1] = 1;
int fib(int n) {
if(dp[n]) return dp[n];
else return dp[n] = fib(n - 1) + fib(n - 2); //先把dp[n]附值再傳回
}
```
時間複雜度$O(N)$
空間複雜度$O(N)$
也可以用迴圈解
```cpp=
int dp[n + 1];
dp[0] = dp[1] = 1;
for(int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
```
時間複雜度$O(N)$
空間複雜度$O(N)$
實際複雜度會因題目不同而不同
---
## 路徑總數
### DP 解
```cpp=
vector<vector<int>> dp(N + 1, vector<int>(M + 1, 0));
dp[0][1] = 1;
for(int i = 1; i <= N; i++) {
for(int j = 1; j <= M; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
```
時間複雜度$O(NM)$
空間複雜度$O(NM)$
空間複雜度似乎不太妙,可不可以縮減一下?
---
### 滾動DP解
```cpp=
vector<int> dp(M + 1, 0);
dp[1] = 1;
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= M; j++) {
dp[j] += dp[j - 1];
}
}
cout << dp[M] << '\n';
```
時間複雜度$O(NM)$
空間複雜度$O(M)\; or \; O(min(M, N))$
---
## LCS(最長共同子序列)
<!-- 我不知道怎麼解釋-->
子序列定義:在一個字串中,去除 $\geq 0$ 個字元但不破壞其他字元的相對位置,此新字串即為原字串之子字串。
例:$fdcs$ 的子字串有:$\{\phi, f, d, c, s, fd, fc, fs, dc, ds, cs, fdc, fds, fcs, dcs, fdcs\}$
最長共同子字串:$n$ 個字串共有且長度最長的子字串 (以本例來看,$n = 2$)
### 轉移式
{$$dp_{ij} = \begin{cases}dp_{i-1,j-1} + 1 \quad &if& a_i = b_j \\ max(dp_{i-1, j}, dp_{i, j - 1})\quad &else\end{cases}$$}
### DP解
```cpp=
vector<vector<int>> dp(a.size() + 1, vector<int>(b.size() + 1, 0));
for(int i = 1; i <= a.size(); i++) {
for(int j = 1; j <= b.size(); j++) {
if(a[i] == b[j])
dp[i][j] = dp[i -1][j - 1] + 1;
else
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
```
**a的長為N, b的長為M**
時間複雜度$O(NM)$
空間複雜度$O(NM)$
### 滾動DP
```cpp=
vector<vector<int>> dp(2, vector<int>(b.size() + 1, 0));
for(int i = 1; i <= a.size() ;i++) {
for(int j = 1;j <= b.size() ;j++) {
if(a[i] == b[j])
dp[i&1][j] = dp[~i&1][j - 1] + 1;
else
dp[i&1][j] = max(dp[~i&1][j], dp[i&1][j-1]);
}
}
```
**a的長為N, b的長為M**
時間複雜度$O(NM)$
空間複雜度$O(M)$
---
## 背包問題
來看到`DP`的一個經典例題,背包問題。
#### 問題簡述
給予一個容量為`k`的背包。
和一些物品,每個物品都有其大小和價值。
問再不超過背包容量的條件下,能裝入的最大價值。
### 0/1背包
每項物品都只有一個
背包容量為`M`,有`N`個物品。
$v[i] \implies 第i個物品的價值$
$w[i] \implies 第i個物品的重量$
#### 轉移式
{
$$dp[i][j] = \begin{cases}dp[i-1][j]\quad &if\ j< w[i]\\max(dp[i-1][j],v[i] +dp[i-1][j-w[i]])&else\end{cases}$$
}
#### DP解
```cpp=
vector<int> v(N), w(N);
vector<vector<int>> dp(N, vector<int>(M + 1, 0));
for(int i = 0; i < N; i++) {
for(int j = 0; j < w[i]; j++)
dp[i][j] = dp[i - 1][j];
for(int j = w[i]; j <= M; j++)
dp[i][j] = max(dp[i - 1][j], v[i] + dp[i - 1][j - w[i]]);
}
```
時間複雜度$O(NM)$
空間複雜度$O(NM)$
#### 滾動DP
```cpp=
vector<int> v(N), w(N);
vector<int> dp(M + 1, 0);
for(int i = 0; i < N ;i++) {
for(int j = M; j >= w[i]; j--)
dp[j] = max(dp[j], v[i] + dp[j - w[i]]);
}
```
時間複雜度$O(NM)$
空間複雜度$O(M)$
:::info
Q: 為什麼要從後往前DP呢?
:::
### 無限背包
從前往後`DP`, 即可。
```cpp=
vector<int> v(N), w(N);
vector<int> dp(M + 1, 0);
for(int i = 0; i < N ;i++) {
for(int j = w[i]; j <= M; j++)
dp[j] = max(dp[j], v[i] + dp[j - w[i]]);
}
```
時間複雜度$O(NM)$
空間複雜度$O(M)$
---
### 有限背包
`DP,位元運算`
#### 一開始的想法
把每一個物品都做一次`O/1背包`。
```cpp=
vector<int> v(N), w(N), k(N);
vector<int> dp(M + 1, 0);
for(int i = 0; i < N;i ++)
for(int t = 0; t < k[i]; t++)
for(int j = M; j >= w[i]; j--)
dp[j] = max(dp[j], v[i] + dp[j - w[i]]);
```
時間複雜度$O(kMN)$
空間複雜度$O(M)$
#### 嘗試優化
把物品的數量拆成二的倍數。
```cpp=
vector<int> v(N), w(N), k(N);
for(int i = 0; i < N; i++) {
int rest = k[i];
int weight, val;
for(int t = 1; t <= rest; t <<= 1) { //t *= 2
rest -= t;
weight = t * w[i], val = t * val[i];
for(int j = M;j >= weight; j--)
dp[j] = max(dp[j], val + dp[j - weight]);
}
weight = t * w[i], val = t * v[i];
for(int j = M; j >=; j++) {
dp[j] = max(dp[j], val + dp[j - weight]);
}
}
```
時間複雜度$O(NM\log k)$
空間複雜度$O(M)$
---
## 補充
### [無限階梯](http://203.64.191.163/ShowProblem?problemid=a650)
費氏數列的延伸
可以前進`K`和不前進,就是從前`K`格和當前格的方法相加。
:::spoiler code
```cpp=
const int mod = 1e9 +7;
int N, M;
cin >> N >> M;
V<V<int>> dp(2, V<int> (N, 0));
dp[1][0] = 1; //dp[~0&1][0] = 1;
for(int i = 0, a; i < M; i++) {
cin >> a;
for(int j = 0; j < N; j++) {
int cur_pos = (j + a) % N;
dp[i&1][cur_pos] = (dp[~i&1][cur_pos] + dp[~i&1][j]);
dp[i&1][cur_pos] %= mod;
}
for(auto &j : dp[i&1]) cout << j << ' '; cout << endl;
}
cout << dp[~M&1][0] - 1<< endl;
```
:::
---
## 小結
`DP`的題目很多變,就多練題目,然後通靈:poop:
#### 補充:陣列的初始化
`memset(arr, 0, sizeof(arr)) // 多維陣列都一樣`