---
tags: Programming Contest
---
# Digit DP
最近遇到不少 digit dp 的題目,而網上的題解通常是 top-down + 遞歸的方法,但有一些題目,例如 Valid payments,如果硬要用遞歸來解,我覺得解法很不直覺。因此在這篇文章我希望整理並統一我 digit dp 的寫法:bottom up + 遞推。
## [Valid payments](https://atcoder.jp/contests/abc182/tasks/abc182_f)
題目相當於在問:「所有 $A$ 進制的 $N$ 位數中(允許 leading 0)有幾個數 $r$ 滿足 $r$ 與 $X + r$ 沒有共同不為 $0$ 的 digit」,即 $\forall_{0 \le i \lt N}, r_i = 0 \lor (X + r)_i = 0$。
設 $y = X + r$,$c$ 是 carry,直式加法如下:
$$
\begin{array}{r}
&c_3 &c_2 &c_1 &0 \\
&x_3 &x_2 &x_1 &x_0 \\
+\quad
&r_3 &r_2 &r_1 &r_0 \\
\hline
&y_3 &y_2 &y_1 &y_0
\end{array}
$$
其中每一位 $i$ 都滿足($m_i$ 是第 $i$ 位的上界):
$$
\begin{align}
y_i &= (c_i + x_i + r_i) \pmod {m_i} \\
c_{i + 1} &= \lfloor \frac{c_i + x_i + r_i}{m_i} \rfloor
\end{align}
$$
設 $dp[i, c]$ = number of valid ways to fill $r_{i - 1} \cdots r_1 r_0$ and $c_i = c$。題目所求即為 $dp[N, 0]$。枚舉 $r_i$ 所有可能的值,我們檢查 $r_i$ 與 $y_i$ 是不是滿足題目要求的 $r_i = 0 \lor y_i = 0$,我們得到遞推轉移:
$$
\text{if } r_i = 0 \lor y_i = 0 \text{ then } dp[i, c] \to dp[i + 1, c']
$$
```python
for i in range(N):
for c in range(2):
for r in range(m[i]):
val = c + x[i] + r
y = val % mx[i]
if r == 0 or y == 0:
new_c = val // mx[i]
dp[i + 1][new_c] += dp[i][c]
```
概念上就是如此,不過這題只有這樣會 TLE,還需做一個優化,細節請參[完整題解](https://hackmd.io/@amoshyc/BkKpztrtP#%E8%A7%A3%E6%B3%95-1)。Valid payments 是我第一次遇到 digit dp,到處找詢資料,花了許多時間才研究出直覺的 bottom up + 遞推的做法,該方法作為我 digit dp 的核心,後面的題目都由此擴展。
## [Digit Sum](https://atcoder.jp/contests/dp/tasks/dp_s)
整數 $1$ ~ $K$ 之間,有多少個整數 $X$ 滿足:digit sum 是 $D$ 的倍數。
讓我們將 $K, X$ 表示成陣列,以 $K=2345$ 為例:
$$
\begin{array}{r}
&k_3 &k_2 &k_1 &k_0 \\
&2 &3 &4 &5 \\
&x_3 &x_2 &x_1 &x_0
\end{array}
$$
設 $dp[i, s, f]$ = number of valid ways to fill $x_{i - 1} \cdots x_1x_0$ and
1. digit sum $(\sum_{j=0}^{i-1} x_j) \bmod D = s$
2. $(f = 0): x_{i-1} \cdots x_1 x_0 \le k_{i-1} \cdots k_1 k_0$
$(f = 1): x_{i-1} \cdots x_1 x_0 \gt k_{i-1} \cdots k_1 k_0$
相較於前一題我們多了一個上界 $K$ 的限制,於是用一個狀態 $f$ 來記錄之。以 $K = 2345$ 為例,$dp[2, 0, 0]$ 代表二位數中有多少數小於或等於 45 且 digit sum $\bmod D = 0$;$dp[2, 0, 1]$ 代表二位數中有多少數大於 $45$ 且 digit sum $\bmod D = 0$;而 $(dp[2, 0, 0] + dp[2, 0, 1])$ 代表所有二位數中有多少個數的 digit sum $\bmod D = 0$。
枚舉 $x_i$ 的所有可能的值 $d (0 \le d \le 9)$,我們有以下遞推:
$$
dp[i, s, f] \to dp[i + 1, (s + d) \bmod D, f']
$$
其中 $f'$ 代表新的數字 $x_i x_{i-1} \cdots x_1 x_0$ 是不是大於 $k_i k_{i-1} \cdots k_1 k_0$。思考一下可以發現,只有當 $(d \gt k_i) \lor (d = k_i \land f = 1)$ 時 $f'$ 會是 1。題目所求為 $dp[N, 0, 0] - 1$。最後的 $-1$ 是因為 dp 會把 $X=0$ 也算是答案,但題目問得是 $1$ ~ $K$。
```python
K = [int(x) for x in reversed(input())]
N = len(K)
D = int(input())
M = 10 ** 9 + 7
dp = [[[0, 0] for _ in range(D)] for _ in range(N + 1)]
dp[0][0][0] = 1
for i in range(N):
for s in range(D):
for f in range(2):
for d in range(10):
new_s = (s + d) % D
new_f = int((d > K[i]) or (d == K[i] and f == 1))
dp[i + 1][new_s][new_f] += dp[i][s][f]
dp[i + 1][new_s][new_f] %= M
print((dp[N][0][0] - 1 + M) % M) # subtract the 0
```
## [不要 62](https://vjudge.net/problem/HDU-2089)
問區間 $[n, m]$ 之間的數有多少個滿足:
1. 沒有字元 4
2. 沒有字串 62
設 $f(K)$ 可以求出 $[0, K]$ 有多少個數 $X$ 滿足題意,則題目所求是 $f(m) - f(n - 1)$。而 $f(K)$ 可以用 digit dp 實現。如同以往,我們將 $K, X$ 表示成陣列:
$$
\begin{array}{r}
&k_3 &k_2 &k_1 &k_0 \\
&x_3 &x_2 &x_1 &x_0
\end{array}
$$
因為 digit dp 是從低位往高位算,所以我們需要記錄上一位 $x_{i - 1}$ 是不是 2。若是的話,則下一位 $x_i$ 不能用 6。
設 $dp[i, p, f]$ = number of valid ways to fill $x_{i - 1} \cdots x_1 x_0$ and
1. $(p = 0): x_{i - 1} \ne 2$
$(p = 1): x_{i - 1} = 2$
2. $(f = 0): x_{i-1} \cdots x_1 x_0 \le k_{i-1} \cdots k_1 k_0$
$(f = 1): x_{i-1} \cdots x_1 x_0 \gt k_{i-1} \cdots k_1 k_0$
$f(K)$ 就是 $dp[N, 0, 0] + dp[N, 1, 0]$,其中 $N$ 是 $K$ 的位數。如何前幾題,我們枚舉 $x_i$ 所有可能的值為 $d$,檢查是不是合法的,可以得到遞推轉移:
```cpp
for (int i = 0; i < N; i++) {
for (int p = 0; p <= 1; p++) { // x[i - 1] is 2 or not
for (int f = 0; f <= 1; f++) {
for (int d = 0; d < 10; d++) { // x[i]
if (d == 4)
continue;
if (p == 1 && d == 6)
continue;
int new_p = int(d == 2);
int new_f = int(d > k[i] || (d == k[i] && f == 1));
dp[i + 1][new_p][new_f] += dp[i][p][f];
}
}
}
}
```
[完整 AC Code](https://vjudge.net/solution/28799287/dc8vqFwFn15Nrq1Xv2xd)。
## [Windy 數](https://vjudge.net/problem/LibreOJ-10165)
定義 Windy 數為該數的數字(不含 leading 0)與數字之間差至少 2,問 $[a, b]$ 之間有多少 Windy 數?
如同上一題,我們轉換題目為求 $f(K)$ = $[0, K]$ 之間有多少數是 Windy 數。不同的是我們不只記錄前一位是否是 2,而是記錄前一位的數字是什麼。
設 $dp[i, p, f]$ = number of valid ways to fill $x_{i - 1} \cdots x_1 x_0$ and
1. $x_{i - 1} = p$
2. $(f = 0): x_{i-1} \cdots x_1 x_0 \le k_{i-1} \cdots k_1 k_0$
$(f = 1): x_{i-1} \cdots x_1 x_0 \gt k_{i-1} \cdots k_1 k_0$
如果我們直接使用上一題的邏輯來遞推轉移會發現答案是錯的!這是因為 digit dp 將數字視為有 leading zero 的,例如 $K = 1234$ 時,$36$ 會被視為 $0036$ 在處理。這個特性在上一題並不影響答案,但這題會。$0036$ 是 Windy 數但 digit dp 會將之視為不是,因為他有 $00$ 不滿足數字差至少為 2,因此我們得修改我們的 dp。
關於這種情況有不少種解法,而我的解法是將 leading zero 用另一個字元 $\bullet$ 代替,將之與數字中間的 0 分開。例如 $36$ 會被視為 $\bullet \bullet 36$。而 $103$ 會被視為 $\bullet103$。
當我們枚舉 $x_i$ 時,我們用以下規則來確保製造出合法的 Windy 數:
1. 若 $x_{i - 1}$ 是 $\bullet$,則 $x_i$ 必得填 $\bullet$,因為 leading zero 出現,其左邊都得是 leading zero。
2. 若 $x_{i - 1}$ 是 0,則 $x_i$ 不可填 $\bullet$,因為 0 不可位於數的開頭。
3. 若 $x_{i - 1}$ 是其他數字時,則 $x_i$ 可以填 $\bullet$ 也可以填其他數字,只要滿足題意。
僅當這些規則成立時,這才是一個有效的 $x_i$,此時才遞推轉移:
$$
\text{if } (1) \land (2) \land (3) \text{, then } dp[i, p, f] \to dp[i + 1, x_i, f']
$$
$f'$ 的計算跟之前稍為不同,$f'$ 代表的是 $x_i x_{i-1} \cdots x_1 x_0 \gt k_i k_{i-1} \cdots k_1 k_0$ 成立與否。如果 $x_i$ 填 $\bullet$ 的話,則式子必不成立,即 $f' = 0$。因此 $f' = 1$ 的情況是 $((x_i \gt k_i) \lor (x_i = k_i \land f = 1)) \land (x_i \ne \bullet)$。
而 $f(K)$ 就是 $dp[N, \bullet, 0] + \sum_{p = 1}^9 dp[N, p, 0]$,注意到 $dp[N, 0, 0]$ 並不是答案一部份,因為現在數字不會以 0 開頭。實作上我將 $\bullet$ 用值 10 代替。
```cpp
for (int i = 1; i < N; i++) {
for (int p = 0; p < 11; p++) { // x[i - 1]
for (int f = 0; f <= 1; f++) {
for (int d = 0; d < 11; d++) { // x[i]
bool valid = false;
if (p == 0) {
valid = (2 <= d && d <= 9);
} else if (p == 10) {
valid = (d == 10);
} else {
valid = (abs(p - d) >= 2 || d == 10);
}
if (valid) {
int new_f = (d > k[i] || (d == k[i] && f == 1)) && (d != 10);
dp[i + 1][d][new_f] += dp[i][p][f];
}
}
}
}
}
int res = 0;
for (int p = 1; p < 11; p++) {
res += dp[N][p][0];
}
```
[完整 AC Code](https://vjudge.net/solution/28821098/GsEK6qTQySb2N8coMoZn)。
:::success
[< 回到所有題解](https://hackmd.io/@amoshyc/HkWWepaPv/)
:::