---
tags: Programming Contest
---
# Educational DP Contest 題解(A ~ M)
[題目連結](https://atcoder.jp/contests/dp)
## 心得
practice。
最近似乎遞推的寫法比較主流,所以我也嘗試用遞推來寫,而非我習慣的遞歸。
## A. [Frog 1](https://atcoder.jp/contests/dp/tasks/dp_a)
簡單 dp。
```python
N = int(input())
H = [int(x) for x in input().split()]
dp = [10 ** 10 for _ in range(N)]
dp[0] = 0
for i in range(N):
if i + 1 < N:
dp[i + 1] = min(dp[i + 1], dp[i] + abs(H[i + 1] - H[i]))
if i + 2 < N:
dp[i + 2] = min(dp[i + 2], dp[i] + abs(H[i + 2] - H[i]))
print(dp[N - 1])
```
## B. [Frog 2](https://atcoder.jp/contests/dp/tasks/dp_b)
簡單 dp。
```python
N, K = map(int, input().split())
H = [int(x) for x in input().split()]
dp = [10 ** 10 for _ in range(N)]
dp[0] = 0
for i in range(N):
for k in range(1, min(K + 1, N - i)):
dp[i + k] = min(dp[i + k], dp[i] + abs(H[i + k] - H[i]))
print(dp[N - 1])
```
## C. [Vacation](https://atcoder.jp/contests/dp/tasks/dp_c)
$dp[i, j]$ 為經過 $[0, i)$ 這些天,且第 $i - 1$ 天是活動 $j$ 的最大 happiness,然後照著題目的限制遞推。
```python
N = int(input())
# dp[i, j] = the maximum value of day [0, i), and perform j on day i - 1
dp = [[0, 0, 0] for _ in range(N + 1)]
dp[0] = [0, 0, 0]
for i in range(N):
gain = [int(x) for x in input().split()]
for j in range(3): # today
for k in range(3): # tomorrow
if j != k:
dp[i + 1][k] = max(dp[i + 1][k], dp[i][j] + gain[j])
print(max(dp[-1]))
```
## D. [Knapsack 1](https://atcoder.jp/contests/dp/tasks/dp_d)
經典 0/1 背包。
```python
N, W = map(int, input().split())
ws, vs = [], []
for _ in range(N):
w, v = map(int, input().split())
ws.append(w)
vs.append(v)
# dp[i, j] = maximum value while
# total weight is j and only [0, i) items are considered
dp = [[0 for _ in range(W + 1)] for _ in range(N + 1)]
for i in range(N):
for j in range(W):
# don't use item i
dp[i + 1][j] = max(dp[i + 1][j], dp[i][j])
# use item i
if j + ws[i] <= W:
dp[i + 1][j + ws[i]] = max(dp[i + 1][j + ws[i]], dp[i][j] + vs[i])
print(max(dp[N]))
```
## E. [Knapsack 2](https://atcoder.jp/contests/dp/tasks/dp_e)
還是 0/1 背包,但 $w_i$ 很大,$v_i$ 很小。
設 $dp[i, j]$ 為總價值為 $j$ 時,最小的物品重量和,只有物品 $[0, i)$ 有被考慮。
跟 0/1 背包一樣,考慮第 $i$ 件物品要用或不用,可以得到轉移方程。
最終結果為從大到小迭代總價值,看哪一個價值所需要的總重量在 $W$ 以內。
```python
N, W = map(int, input().split())
ws, vs = [], []
for _ in range(N):
w, v = map(int, input().split())
ws.append(w)
vs.append(v)
V = max(vs) * N
# dp[i, j] = minimum weight when
# total value is j and only items [0, i) are considered
INF = 10 ** 10
dp = [[INF for _ in range(V + 1)] for _ in range(N + 1)]
dp[0][0] = 0
for i in range(N):
for j in range(V + 1):
# don't use item i
dp[i + 1][j] = min(dp[i + 1][j], dp[i][j])
# use item i
if j + vs[i] <= V:
dp[i + 1][j + vs[i]] = min(dp[i + 1][j + vs[i]], dp[i][j] + ws[i])
for v in range(V, -1, -1):
if dp[N][v] <= W:
print(v)
break
```
## F. [LCS](https://atcoder.jp/contests/dp/tasks/dp_f)
經典的 LCS。這題遞推實在不怎麼好寫,所以還是用標準遞歸的寫法。
實作上為了方便,我在 $S, T$ 的開頭補了 1 個相異的特殊字元。
```python
S = '#' + input()
T = '$' + input()
dp = [[0 for _ in range(len(T))] for _ in range(len(S))]
prev = [[-1 for _ in range(len(T))] for _ in range(len(S))]
for i in range(1, len(S)):
for j in range(1, len(T)):
if S[i] == T[j]:
dp[i][j] = dp[i - 1][j - 1] + 1
prev[i][j] = 0
elif dp[i - 1][j] > dp[i][j - 1]:
dp[i][j] = dp[i - 1][j]
prev[i][j] = 1
else:
dp[i][j] = dp[i][j - 1]
prev[i][j] = 2
lcs = []
i, j = len(S) - 1, len(T) - 1
while i >= 0 and j >= 0:
if prev[i][j] == 0:
lcs.append(S[i])
i -= 1
j -= 1
elif prev[i][j] == 1:
i -= 1
elif prev[i][j] == 2:
j -= 1
else:
break
print(''.join(map(str, reversed(lcs))))
```
## G. [Longest Path](https://atcoder.jp/contests/dp/tasks/dp_g)
經典的 DAG 最長路徑。
設 $dp[v]$ 為停在 vertex $v$ 的所有路徑中,最長的長度。考慮 $v$ 的 incoming edges $E^-_v$,我們有:
$$
dp[v] = \max_{u \in E^-_v} dp[u] + 1
$$
根據轉移方程,可知這個 $dp$ 的計算順序為 DAG 的 topological order。
```python
from collections import deque
N, M = map(int, input().split())
in_deg = [0 for v in range(N)]
graph = [[] for v in range(N)]
for _ in range(M):
u, v = map(int, input().split())
u, v = u - 1, v - 1
graph[u].append(v)
in_deg[v] += 1
# dp[v] = length of the longest path ending at v
dp = [0 for _ in range(N)]
# topological order
que = deque([v for v, deg in enumerate(in_deg) if deg == 0])
while len(que) > 0:
u = que.popleft()
for v in graph[u]:
dp[v] = max(dp[v], dp[u] + 1)
in_deg[v] -= 1
if in_deg[v] == 0:
que.append(v)
print(max(dp))
```
## H. [Grid 1](https://atcoder.jp/contests/dp/tasks/dp_h)
這題一直出現一直出現……
設 $dp[r, c]$ 為從起點到 $(r, c)$ 的方法數。
```python
H, W = map(int, input().split())
A = [input() for _ in range(H)]
M = 10 ** 9 + 7
dp = [[0 for _ in range(W)] for _ in range(H)]
dp[0][0] = 1
for r in range(H):
for c in range(W):
if r + 1 < H and A[r + 1][c] == '.':
dp[r + 1][c] += dp[r][c]
dp[r + 1][c] %= M
if c + 1 < W and A[r][c + 1] == '.':
dp[r][c + 1] += dp[r][c]
dp[r][c + 1] %= M
print(dp[H - 1][W - 1])
```
## I. [Coins](https://atcoder.jp/contests/dp/tasks/dp_i)
給定 $N$ 個 coins,coin $i$ 得到 head 的機率是 $p_i$。依序拋完所有 coins,問得到「head 數量比 tail 多」的機率是多少?
設 $dp[i, j]$ 為拋完 coins $[0, i)$,head 的數量為 $j$ 的機率。考慮 coin i 的結果,得到轉移方程。
```python
N = int(input())
P = [float(x) for x in input().split()]
# dp[i][j] = Pr(number of head = j) after [0, i) coins are tossed
dp = [[0.0 for _ in range(N + 1)] for _ in range(N + 1)]
dp[0][0] = 1.0
for i in range(N):
for j in range(N + 1):
# coin i is tail
dp[i + 1][j] += dp[i][j] * (1 - P[i])
# coin i is head
if j + 1 <= N:
dp[i + 1][j + 1] += dp[i][j] * P[i]
ans = 0.0
for n_head in range(N + 1):
if n_head > N - n_head:
ans += dp[N][n_head]
print('{:.15f}'.format(ans))
```
## J. [Sushi](https://atcoder.jp/contests/dp/tasks/dp_j)
給定 N 個盤子,盤子 $i$ 上有壽司 $a_i$ 個。定義一次操作為 uniform 地選出一個盤子,若盤子上有壽司,吃掉其中一個。問吃完所有壽司的期望操作次數。
這題最重要的觀察是 **是哪一個盤子並不重要的,重要的是各種壽司數的盤子有幾個**,因為盤子是 uniform 地選。
設 $E(a, b, c)$ 為目前有 $a$ 個壽司數為 1 的盤子、 $b$ 個壽司數為 2 的盤子、 $c$ 個壽司數為 3 的盤子,將壽司吃完的期望操作次數。考慮接下來的一次操作,我們有
1. $\frac{a}{N}$ 的機率選到壽司數為 1 的盤子,吃掉一個壽司後,轉移到 $E(a - 1, b, c)$。
2. $\frac{b}{N}$ 的機率選到壽司數為 2 的盤子,吃掉一個壽司後,轉移到 $E(a + 1, b - 1, c)$。
3. $\frac{c}{N}$ 的機率選到壽司數為 3 的盤子,吃掉一個壽司後,轉移到 $E(a, b + 1, c - 1)$
4. $\frac{N - a - b - c}{N}$ 的機率選到壽司數為 0 的盤子,轉移到 $E(a, b, c)$。
數學式如下:
$$
\begin{align}
E(a, b, c) =
&1 +
\frac{a}{N} E(a - 1, b, c) +
\frac{b}{N} E(a + 1, b - 1, c) + \\
&\frac{c}{N} E(a, b + 1, c - 1) +
\frac{N - a - b - c}{N} E(a, b, c)
\end{align}
$$
整理後得轉移方程:
$$
E(a, b, c) = \frac{N}{a + b + c} \left(
1 +
\frac{a}{N} E(a - 1, b, c) +
\frac{b}{N} E(a + 1, b - 1, c) +
\frac{c}{N} E(a, b + 1, c - 1)
\right)
$$
實作上使用 top-down dp,這題用 python 時間會非常緊,建議使用 c++。
```cpp
#include <iostream>
#include <vector>
#include <iomanip>
#include <algorithm>
using namespace std;
using real = long double;
int N;
real dp[301][301][301];
real E(int a, int b, int c) {
if (dp[a][b][c] >= 0) {
return dp[a][b][c];
}
if (a == 0 && b == 0 && c == 0) {
dp[a][b][c] = 0;
return 0;
}
real res = 1;
if (a >= 1) res += real(a) / N * E(a - 1, b, c);
if (b >= 1) res += real(b) / N * E(a + 1, b - 1, c);
if (c >= 1) res += real(c) / N * E(a, b + 1, c - 1);
res *= real(N) / (a + b + c);
dp[a][b][c] = res;
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> N;
vector<int> cnt(4, 0);
for (int i = 0; i < N; i++) {
int a;
cin >> a;
cnt[a]++;
}
for (int i = 0; i <= 300; i++) {
for (int j = 0; j <= 300; j++) {
for (int k = 0; k <= 300; k++) {
dp[i][j][k] = -1.0;
}
}
}
cout << fixed << setprecision(10) << E(cnt[1], cnt[2], cnt[3]) << endl;
return 0;
}
```
## K. [Stones](https://atcoder.jp/contests/dp/tasks/dp_k)
給定 $N$ 個正整數 $A$,Tora 與 Jiro 輪流從 $K$ 個石頭中拿走一些,拿走的數量必需是 $A$ 中的一項。Tora 先拿,誰沒石頭拿誰就輸山,若雙方都是最優地拿,誰會贏?
$dp[i, j]$ = 若剩下 $i$ 個石頭,現在換 $j$(Tora = $0$, Jiro = $1$)拿,$j$ 會不會贏(win = True, lose = False)。
根據題意,$dp[0, 0] = dp[0, 1] = False$。讓我們考慮 $dp[i, 0]$ 什麼時候會是 True?是前一步的狀態中,有任何一個狀態 Jiro 是輸的!寫成式子:
```python
dp[i, 0] = True if any(dp[i - a, 1] is False for a in A) else False
```
同理,$dp[i, 1]$ 也有:
```python
dp[i, 1] = True if any(dp[i - a, 0] is False for a in A) else False
```
實作上小心邊界。這題用遞歸比遞推好懂。另外發現拿 tabulate 來印 dp 表格挺方便的。
```python
N, K = map(int, input().split())
A = [int(x) for x in input().split()]
# dp[i][j] = True if j will win when it's j's turn and i stones remained
dp = [[-1, -1] for i in range(K + 1)]
dp[0][0] = False
dp[0][1] = False
for i in range(1, K + 1):
dp[i][0] = True if any(dp[i - a][1] is False for a in A if i - a >= 0) else False
dp[i][1] = True if any(dp[i - a][0] is False for a in A if i - a >= 0) else False
# from tabulate import tabulate
# print(tabulate(dp, showindex=True, headers=['Taro', 'Jiro']))
print('First' if dp[K][0] else 'Second')
```
## L. [Deque](https://atcoder.jp/contests/dp/tasks/dp_l)
給定長度為 $N$ 的 deque $A$,Taro 與 Jiro 輪流:拿走 $A$ 的開頭抑或結尾,直到 $A$ 被拿完。最終 Taro 拿走的數字和為 $X$,Jiro 是 $Y$。若 Taro 想要最大化 $X - Y$,Jiro 想要最小化 $X - Y$,問最後 $X - Y$ 是多少?
設 $f(i, j, k)$ 為只考慮 $A$ 的封閉區間 $[i, j]$ 且現在換 $k$ 拿 (Taro = 1, Jiro = 0),得到的 $X - Y$。
根據題意,枚舉 $k$ 能拿的 2 個數字,得到轉移方程:
$$
\begin{align}
f(i, j, 1) = max(f(i + 1, j) + A_i, f(i, j - 1) + A_j) \\
f(i, j, 0) = min(f(i + 1, j) - A_i, f(i, j - 1) - A_j)
\end{align}
$$
top-down dp,用 python 會 TLE。
```cpp
#include <iostream>
#include <tuple>
#include <unordered_map>
#include <vector>
using namespace std;
using ll = long long;
const ll INF = 1e18;
int N;
ll A[3000];
ll dp[3000][3000][2];
ll f(int i, int j, bool is_tora) {
if (dp[i][j][is_tora] != INF) {
return dp[i][j][is_tora];
}
ll res = 0;
if (i == j) {
res = (is_tora) ? A[i] : -A[i];
} else if (is_tora) {
res = max(f(i + 1, j, false) + A[i], f(i, j - 1, false) + A[j]);
} else {
res = min(f(i + 1, j, true) - A[i], f(i, j - 1, true) - A[j]);
}
dp[i][j][is_tora] = res;
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> N;
for (int i = 0; i < N; i++) {
cin >> A[i];
}
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
dp[i][j][0] = INF;
dp[i][j][1] = INF;
}
}
cout << f(0, N - 1, true) << endl;
return 0;
}
```
## M. [Candies](https://atcoder.jp/contests/dp/tasks/dp_m)
將整數 $K$ 拆成 $N$ 個非負整數的和,且第 $i$ 個數 $x_i$ 滿足 $x_i \in [0, a_i]$。問方法數。
這題用遞推要優化似乎很難想,讓我們還是使用遞歸。
設 $dp[i, j]$ 為使用 $x_0, x_1, \cdots, x_i$ 組出 $j$ 的方法數,題目所求即為 $dp[N - 1, K]$。
考慮 $x_i$ 是多少:下界是 $0$,上界是 $min(a_i, j)$,我們得到轉移:
$$
dp[i, j]
= \sum_{k=0}^{min(a_i, j)} dp[i - 1, j - k]
= \sum_{k=j - min(a_i, j)}^j dp[i - 1, k]
= \sum_{k=max(0, j - a_i)}^j dp[i - 1, k]
$$
整體時間 $O(NK^2)$ 會 TLE。我們嘗試優化,發現右式就是之前 dp 值的某個區間和,因此如果我們使用 BIT/SegTree 來存 dp 表格,我們能將時間降至 $O(NKlgK)$:
```cpp
#include <atcoder/fenwicktree>
#include <atcoder/modint>
#include <iostream>
#include <vector>
using namespace std;
using mint = atcoder::modint1000000007;
using BIT = atcoder::fenwick_tree<mint>;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int N, K;
cin >> N >> K;
vector<int> A(N);
for (int i = 0; i < N; i++) {
cin >> A[i];
}
auto dp = vector<BIT>(N, BIT(K + 1));
for (int j = 0; j <= A[0]; j++) {
dp[0].add(j, +1);
}
for (int i = 1; i < N; i++) {
for (int j = 0; j <= K; j++) {
dp[i].add(j, dp[i - 1].sum(j - min(A[i], j), j + 1));
}
}
cout << dp[N - 1].sum(K, K + 1).val() << endl;
return 0;
}
```
另外我們也可以仔細的觀察轉移的右式,會發現他基本上就是 $dp[i - 1]$ 中,$j$ 以左長度 $a_i$ 的 window 的和,超出邊界的部份不算,因此我們用一個變數 `window_sum` 去記錄這個和,`window_sum` 會隨著 $j$ 的迭代更新。
```python
N, K = map(int, input().split())
mod = 10 ** 9 + 7
A = [int(x) for x in input().split()]
dp = [[0 for _ in range(K + 1)] for _ in range(N)]
for j in range(A[0] + 1):
dp[0][j] = 1
for i in range(1, N):
window_sum = 0
for j in range(K + 1):
window_sum += dp[i - 1][j]
if j - A[i] - 1 >= 0:
window_sum -= dp[i - 1][j - A[i] - 1]
window_sum %= mod
dp[i][j] = window_sum
print(dp[N - 1][K])
```
:::success
[< 回到所有題解](https://hackmd.io/@amoshyc/HkWWepaPv/)
:::