数位 dp 学习笔记(灵神模板)
我谔谔,数位 dp 几年了还不会,学的都是些奇奇怪怪的写法,导致每次比赛遇到数位 dp 的题要么不会,要么写半天。灵神的数位 dp 模板其实很早就有看过,不过当时不是很理解递归的含义于是放弃了,最近重新来回来看发现还是递归好写,不仅简短而且基本都是一个框架,这就可以大大减少思考量,基本只要遇到数位 dp 的题直接套板然后部分修改就好了。
先给出最基本的模板,求的是区间 中满足条件的数的个数:
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;
}
这个版本是允许数字有前导零的,可以这么做的前提是有前导零不会影响答案。
代码中的 是对 转换成字符串后的结果。
参数中的 是指当前在第 位填数字,数字是从最高位开始依次往低位填的。 是一个 变量,如果是 表示前面填的数字都是 对应位上的数字,那么第 位能填的数字只能是 ;否则如果是 表示前面至少存在一个位填的数字小于 对应位上的数字,那么第 位能填的数字可以是 。
dfs(u, high)
返回的是从第 位开始填,第 位前面填的数字是否都贴着上界,所能构造出满足条件的数的数量。边界条件是 等于 的数位长度,此时返回 (有其他条件的话还要判断是否满足)。
和 对应第 位上能填的数字的范围,其中在这个模板中 都是 , 受 的影响。
是为了实现记忆化搜索,其实可以把 扩展成 ,实现时之所以不用记录 那维,是因为当 时必然只会搜一次(标记,这里其实我现在也不是很理解)。
最后调用的方法是 dfs(0, 1)
,一开始置 是因为第 位能填的数字范围只能是 。
再给出另外一个不含前导 的模板,求的是区间 中满足条件的数的个数:
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;
}
参数中的 是一个 变量,如果是 表示第 位之前都没有填过任何数字,表示有前导零;否则如果是 表示第 位之前填过数字,没有前导零。当递归到边界时,如果 表示都没填过数字,返回 ,否则返回 。
当 时,当前第 位可以继续不填数字,即执行 dfs(u + 1, 0, 1)
, 会变成 ,是因为前 位都没填过数字,自然就不会帖着 对应位的上界。
另外第 为最小能填的数取决于 ,当 时,因为此时第 位上要填数字,因此必须从 开始填,否则可以从 开始填。
其他的部分与上一个模板几乎一样,最后调用的方法是 dfs(0, 1, 1)
。
上面两个模板都是解决求某个前缀中满足条件的数的数量,如果询问变成了求 范围内满足条件的数的数量时,我们就可以用前缀和的思想求 dp(R) - dp(L-1)
即可。其中 dp
函数中的内容是:
int dp(int n) {
s = to_string(n);
memset(f, -1, sizeof(f));
return dfs(0, 1, 1);
}
不过这里再给出另外一个模板,直接解决求 范围内满足条件的数的数量:
不考虑前导零:
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;
}
当询问的 和 超过 long long
的范围时,我们需要转成字符串的形式去求 dp(R) - dp(L-1)
,这里 就要涉及到高精度减法了,使用这个模板有一个好处就是可以避免这个减 的处理。
代码中的 和 分别对应 和 转换成字符串的结果。如果 的长度小于 的长度,那么在 前补充 使其与 的长度相等。
参数中的 与 类似,用来表示第 位前填的数字是否都贴着 的下界。
对应的 的取值就会受到 的影响,当 说明第 位只能从 开始填(否则填的数字就会小于 ),否则可以从 开始填。当考虑前导 时,那么可以填的最小数字理应是 。需要注意的是不可以令 ,这里的 和 是第 位可以填的数字的下界和上界,只能受到 和 的影响,即 和 不能被修改。
调用的方法是 dfs(0, 1, 1)
或 dfs(0, 1, 1, 1)
。
下面选一些题来应用这几个模板。
首先考虑数字能否有前导零,虽然题目说的是统计不含前导零的数的数量,但可以发现是否有前导零并不影响结果,这是因为对于满足最低的 s.size()
位与 s
一样,且每一位的数字不超过 limit
的数,很明显补上前导零后这个数还是满足这两个条件。
当 不在最低的 s.size()
位时,那么第 位可以填的数字是 ,注意不能把 对 取最小值。否则 在最低的 s.size()
位时,那么第 位只能填 s
对应位上的数字 t = s[s.size() - (s1.size() - u)] - '0'
,并且只有 满足 时才能填。
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);
}
};
这题很明显不能有前导零,因为前导零会影响到相邻两个数字之差至少为 这个条件。同时我们还要用一个参数记录上一个数字选了什么,记作 。最后在枚举当前可以填的数字 时,只能选满足 的数字 。
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 的话发现做不了。考虑把 中的数按照每位数字之和进行分类,那么最多可以分成 类。然后枚举各位数字之和 ,分别求 中有多少个数的各位数字之和为 且这个数模 等于 。另外可以发现前导零不会影响答案,dp 的参数列表为 dfs(u, s, r, p, high)
,其中 表示前 位的数字之和, 表示前 位构成的十进制数模 的值。
答案就是 。
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/
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/17990474
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效