我谔谔,数位 dp 几年了还不会,学的都是些奇奇怪怪的写法,导致每次比赛遇到数位 dp 的题要么不会,要么写半天。灵神的数位 dp 模板其实很早就有看过,不过当时不是很理解递归的含义于是放弃了,最近重新来回来看发现还是递归好写,不仅简短而且基本都是一个框架,这就可以大大减少思考量,基本只要遇到数位 dp 的题直接套板然后部分修改就好了。
先给出最基本的模板,求的是区间 $[0,n]$ 中满足条件的数的个数:
int dfs(int u, int high) {
if (u == s.size()) return 1;
if (!high && f[u] != -1) return f[u];
int l = 0, r = high ? s[u] - '0' : 9;
int ret = 0;
for (int i = l; i <= r; i++) {
ret += dfs(u + 1, high && i == r);
}
if (!high) f[u] = ret;
return ret;
}
这个版本是允许数字有前导零的,可以这么做的前提是有前导零不会影响答案。
代码中的 $s$ 是对 $n$ 转换成字符串后的结果。
参数中的 $u$ 是指当前从第 $u$ 位填数字,数字时是从最高位开始依次往低位填的。$\text{high}$ 是一个 $\text{bool}$ 变量,如果是 $1$ 表示前面填的数字都是 $n$ 对应位上的数字,那么第 $u$ 位能填的数字只能是 $[0, s[u]]$;否则如果是 $0$ 表示前面至少存在一个位填的数字小于 $n$ 对应位上的数字,那么第 $u$ 位能填的数字可以是 $[0, 9]$。
dfs(u, high)
返回的是从第 $u$ 位开始填,第 $u$ 位前面填的数字是否都贴着上界,所能构造出满足条件的数的数量。边界条件是 $u$ 等于 $n$ 的数位长度,此时返回 $1$(有其他条件的话还要判断是否满足)。
$l$ 和 $r$ 对应第 $u$ 位上能填的数字的范围,其中在这个模板中 $l$ 都是 $0$,$r$ 受 $\text{high}$ 的影响。
$f(u)$ 是为了实现记忆化搜索,其实可以把 $f$ 扩展成 $f(u, 0/1)$,实现时之所以不用记录 $\text{high}$ 那维,是因为当 $\text{high} = 1$ 时必然只会搜一次(标记,这里其实我现在也不是很理解)。
最后调用的方法是 dfs(0, 1)
,一开始置 $\text{high} = 1$ 是因为第 $0$ 位能填的数字范围只能是 $[0, s[0]]$。
再给出另外一个不含前导 $0$ 的模板,求的是区间 $[1,n]$ 中满足条件的数的个数:
int dfs(int u, int high, int lead) {
if (u == s.size()) return !lead;
if (!high && !lead && f[u] != -1) return f[u];
int ret = 0;
if (lead) ret = dfs(u + 1, 0, 1);
int l = 0, r = high ? s[u] - '0' : 9;
for (int i = max(l, lead); i <= r; i++) {
ret += dfs(u + 1, high && i == r, 0);
}
if (!high && !lead) f[u] = ret;
return ret;
}
参数中的 $\text{lead}$ 是一个 $\text{bool}$ 变量,如果是 $1$ 表示第 $u$ 位之前都没有填过任何数字,表示有前导零;否则如果是 $0$ 表示第 $u$ 位之前填过数字,没有前导零。当递归到边界时,如果 $\text{lead} = 1$ 表示都没填过数字,返回 $0$,否则返回 $1$。
当 $\text{lead} = 1$ 时,当前第 $u$ 位可以继续不填数字,即执行 dfs(u + 1, 0, 1)
,$\text{high}$ 会变成 $0$,是因为前 $u$ 位都没填过数字,自然就不会帖着 $n$ 对应位的上界。
另外第 $u$ 为最小能填的数取决于 $\text{lead}$,当 $\text{lead} = 1$ 时,因为此时第 $u$ 位上要填数字,因此必须从 $1$ 开始填,否则可以从 $0$ 开始填。
其他的部分与上一个模板几乎一样,最后调用的方法是 dfs(0, 1, 1)
。
上面两个模板都是解决求某个前缀中满足条件的数的数量,如果询问变成了求 $[L, R]$ 范围内满足条件的数的数量时,我们就可以用前缀和的思想求 dp(R) - dp(L-1)
即可。其中 dp
函数中的内容是:
int dp(int n) {
s = to_string(n);
memset(f, -1, sizeof(f));
return dfs(0, -2, 1, 1);
}
不过这里再给出另外一个模板,直接解决求 $[L, R]$ 范围内满足条件的数的数量:
不考虑前导零:
int dfs(int u, int low, int high) {
if (u == s1.size()) return 1;
if (!low && !high && f[u] != -1) return f[u];
int l = low ? s1[u] - '0' : 0, r = high ? s2[u] - '0' : 9;
int ret = 0;
for (int i = l; i <= r; i++) {
ret += dfs(u + 1, low && i == l, high && i == r);
}
if (!low && !high) f[u] = ret;
return ret;
}
考虑前导零:
int dfs(int u, int low, int high, int lead) {
if (u == s1.size()) return !lead;
if (!low && !high && !lead && f[u] != -1) return f[u];
int ret = 0;
if (lead && s1[u] == '0') ret = dfs(u + 1, 1, 0, 1);
int l = low ? s1[u] - '0' : 0, r = high ? s2[u] - '0' : 9;
for (int i = max(l, lead); i <= r; i++) {
ret += dfs(u + 1, low && i == l, high && i == r, 0);
}
if (!low && !high && !lead) f[u] = ret;
return ret;
}
当询问的 $L$ 和 $R$ 超过 long long
的范围时,我们需要转成字符串的形式去求 dp(R) - dp(L-1)
,这里 $L-1$ 就要涉及到高精度减法了,使用这个模板有一个好处就是可以避免这个减 $1$ 的处理。
代码中的 $s_1$ 和 $s_2$ 分别对应 $L$ 和 $R$ 转换成字符串的结果。如果 $s_1$ 的长度小于 $s_2$ 的长度,那么在 $s_1$ 前补充 $0$ 使其与 $s_2$ 的长度相等。
参数中的 $\text{low}$ 与 $\text{high}$ 类似,用来表示第 $u$ 位前填的数字是否都贴着 $L$ 的下界。
对应的 $l$ 的取值就会受到 $\text{low}$ 的影响,当 $\text{low} = 1$ 说明第 $u$ 位只能从 $s_1[u]$ 开始填(否则填的数字就会小于 $L$),否则可以从 $0$ 开始填。当考虑前导 $0$ 时,那么可以填的最小数字理应是 $\min\{ l, \text{lead} \}$。需要注意的是不可以令 $l = \max\{ l, \text{lead} \}$,这里的 $l$ 和 $r$ 是第 $u$ 位可以填的数字的下界和上界,只能受到 $\text{low}$ 和 $\text{high}$ 的影响,即 $l$ 和 $r$ 不能被修改。
调用的方法是 dfs(0, 1, 1)
或 dfs(0, 1, 1, 1)
。
下面选一些题来应用这几个模板。
首先考虑数字能否有前导零,虽然题目说的是统计不含前导零的数的数量,但可以发现是否有前导零并不影响结果,这是因为对于满足最低的 s.size()
位与 s
一样,且每一位的数字不超过 limit
的数,很明显补上前导零后这个数还是满足这两个条件。
当 $u$ 不在最低的 s.size()
位时,那么第 $u$ 位可以填的数字是 $[l, \min\{ r, \text{limit} \}]$,注意不能把 $r$ 对 $\text{limit}$ 取最小值。否则 $u$ 在最低的 s.size()
位时,那么第 $u$ 位只能填 s
对应位上的数字 t = s[s.size() - (s1.size() - u)] - '0'
,并且只有 $t$ 满足 $l \leq t \leq r$ 时才能填。
AC 代码如下:
class Solution {
public:
long long numberOfPowerfulInt(long long start, long long finish, int limit, string s) {
string s1 = to_string(start), s2 = to_string(finish);
s1 = string(s2.size() - s1.size(), '0') + s1;
vector<long long> f(s1.size(), -1);
function<long long(int, int, int)> dfs = [&](int u, int low, int high) {
if (u == s1.size()) return 1ll;
if (!low && !high && f[u] != -1) return f[u];
int l = low ? s1[u] - '0' : 0, r = high ? s2[u] - '0' : 9;
long long ret = 0;
if (s1.size() - u <= s.size()) {
int t = s[s.size() - (s1.size() - u)] - '0';
if (t >= l && t <= r) ret = dfs(u + 1, low && t == l, high && t == r);
}
else {
for (int i = l; i <= r && i <= limit; i++) {
ret += dfs(u + 1, low && i == l, high && i == r);
}
}
if (!low && !high) f[u] = ret;
return ret;
};
return dfs(0, 1, 1);
}
};
这题很明显不能有前导零,因为前导零会影响到相邻两个数字之差至少为 $2$ 这个条件。同时我们还要用一个参数记录上一个数字选了什么,记作 $\text{last}$。最后在枚举当前可以填的数字 $i$ 时,只能选满足 $\left| i - last \right| \geq 2$ 的数字 $i$。
AC 代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 15;
string s1, s2;
int f[N][N];
int dfs(int u, int last, int low, int high, int lead) {
if (u == s1.size()) return !lead;
if (!low && !high && !lead && f[u][last] != -1) return f[u][last];
int ret = 0;
if (lead && s1[u] == '0') ret = dfs(u + 1, -2, 1, 0, 1);
int l = low ? s1[u] - '0' : 0, r = high ? s2[u] - '0' : 9;
for (int i = max(l, lead); i <= r; i++) {
if (abs(i - last) >= 2) ret += dfs(u + 1, i, low && i == l, high && i == r, 0);
}
if (!low && !high && !lead) f[u][last] = ret;
return ret;
}
int main() {
int a, b;
scanf("%d %d", &a, &b);
s1 = to_string(a), s2 = to_string(b);
s1 = string(s2.size() - s1.size(), '0') + s1;
memset(f, -1, sizeof(f));
printf("%d", dfs(0, -2, 1, 1, 1));
return 0;
}
直接 dp 的话发现做不了。考虑把 $1 \sim N$ 中的数按照每位数字之和进行分类,那么最多可以分成 $9 \times 14$ 类。然后枚举各位数字之和 $p$,分别求 $1 \sim N$ 中有多少个数的各位数字之和为 $p$ 且这个数模 $p$ 等于 $0$。另外可以发现前导零不会影响答案,dp 的参数列表为 dfs(u, s, r, p, high)
,其中 $s$ 表示前 $u$ 位的数字之和,$r$ 表示前 $u$ 位构成的十进制数模 $p$ 的值。
答案就是 $\sum\limits_{p=1}^{9 \times 14}{\text{dfs}(0, 0, 0, p, 1)}$。
AC 代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 15, M = 140;
string str;
LL f[N][M][M];
LL dfs(int u, int s, int r, int p, int high) {
if (u == str.size()) return s == p && !r;
if (!high && f[u][s][r] != -1) return f[u][s][r];
int up = high ? str[u] - '0' : 9;
LL ret = 0;
for (int i = 0; i <= up; i++) {
ret += dfs(u + 1, s + i, (r * 10 + i) % p, p, high && i == up);
}
if (!high) f[u][s][r] = ret;
return ret;
}
int main() {
LL n;
scanf("%lld", &n);
str = to_string(n);
LL ret = 0;
for (int i = 1; i < 140; i++) {
memset(f, -1, sizeof(f));
ret += dfs(0, 0, 0, i, 1);
}
printf("%lld", ret);
return 0;
}
参考资料
数位 DP 通用模板:https://www.bilibili.com/video/BV1rS4y1s721/
数位 DP 通用模板 v2.0【力扣双周赛 121】:https://www.bilibili.com/video/BV1Fg4y1Q7wv/
标签:灵神,lead,int,text,s1,high,dp,&&,模板 From: https://www.cnblogs.com/onlyblues/p/17990474