从零基础到独立刷完经典数位 DP 题,沉淀出一套固定骨架的通用写法,换题只需要改状态维度和约束判断,核心逻辑永远不变。
一、核心总思想(永远不变)
前缀和转化:求区间 [L, R] 的答案 = dp(R) - dp(L-1)
dp(n):计算[0, n]中所有满足条件的数的对应结果- 所有数位 DP 题都先拆成「算上界 - 算下界减一」,通用套路永不变
二、通用代码骨架(标准写法)
1. 全局变量区(固定结构)
typedef long long LL;
const int N = 20; // 位数上限,1e18开20足够,二进制开60
vector<int> nums; // 存拆出来的每一位数字
LL f[N][...][...]; // 记忆化数组,维度 = pos + 所有状态参数
// 若需要返回多个值,用 struct 或 pair
维度规则:第一维永远是 pos,后面跟所有状态参数;0/1 状态占一维,枚举类状态(如 last、cnt)对应开大小
2. dfs 函数(核心,永远五步走)
返回值 dfs(int pos, 状态1, 状态2, ..., int t, int l)
{
// 第1步:边界判断(所有位填完)
if(pos == -1)
return 满足条件的边界值;
// 第2步:记忆化查询(算过直接返回,避免重复计算)
if(f[pos][状态1][状态2]...[t][l] != -1)
return f[pos][状态1][状态2]...[t][l];
// 第3步:确定当前位数字上界
int up = t ? nums[pos] : 9; // 二进制改1,B进制改B-1
返回值 res = 初值; // 计数类初值为0,乘积类初值为1
// 第4步:枚举当前位数字,执行状态转移
for(int j = 0; j <= up; j++)
{
// 4.1 计算下一层的固定状态
int nt = t && (j == up); // 下一层是否仍受上界限制
int nl = l && (j == 0); // 下一层是否仍处于前导零
// 4.2 计算下一层的题目自定义状态
新状态1 = 根据j和旧状态推导;
新状态2 = 根据j和旧状态推导;
// 4.3 题目约束校验(不满足直接跳过)
if(不满足题目条件) continue;
// 4.4 递归下一层,合并结果
res += dfs(pos-1, 新状态1, 新状态2, ..., nt, nl);
// 取模题目在此处加 % mod
}
// 第5步:存入记忆化数组,返回当前状态结果
f[pos][状态1][状态2]...[t][l] = res;
return res;
}
3. dp 函数(固定流程)
LL dp(LL n)
{
if(n < 0) return 0; // 边界保护,必加
nums.clear(); // 清空上一次的数位,必加
// 拆数:十进制模10,二进制模2,B进制模B
while(n)
{
nums.push_back(n % 10);
n /= 10;
}
memset(f, -1, sizeof f); // 重置记忆化数组,必须写在dp函数内
// 调用dfs,传入初始状态
return dfs(nums.size()-1, 初始状态1, 初始状态2, ..., 1, 1);
}
4. 主函数(固定写法)
int main()
{
LL L, R;
cin >> L >> R;
LL ans = dp(R) - dp(L-1);
// 取模题目写:LL ans = (dp(R) - dp(L-1) + mod) % mod;
cout << ans << endl;
return 0;
}
三、状态设计通用套路
固定三参数(几乎每题必有)

题目自定义状态(按题目需求新增)
核心原则:所有影响「后续合法性」「后续结果计算」的信息,全部塞进状态维度。

四、常见题型的返回值与合并方式

五、必踩坑点 Checklist
- memset 位置:必须写在
dp函数内部,不能只写在 main;换数字、换目标 d 都要重置缓存 - 数组越界:初始占位值(如 last=11)不能超过数组维度大小,开数组要留余量
- 前导零判断:校验约束用当前层的
l,不是下一层的nl;前导零状态下约束不生效 - 状态传参:递归必须传新计算出的状态,不能误传旧状态
- 前缀和负数:取模题目结果必须
(dp(R) - dp(L-1) + mod) % mod,防止负数 - 位数限制题:如手机号必须 11 位,不足位数直接返回 0,避免前缀和多减
- 输入逆序:部分题目 L 可能大于 R,先执行 swap 再计算
- n<0 保护:dp 函数开头必加
if(n<0) return 0,避免 L=1 时 L-1=-1 触发异常
六、可直接套用的空白模板(纯计数版)
绝大多数纯计数类数位 DP 题,往里填约束和状态即可:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 20;
vector<int> nums;
LL f[N][2][2]; // pos + 自定义状态 + tight + lead
LL dfs(int pos, 自定义状态, int t, int l)
{
if(pos == -1)
return 合法条件 ? 1 : 0;
if(f[pos][自定义状态][t][l] != -1)
return f[pos][自定义状态][t][l];
int up = t ? nums[pos] : 9;
LL res = 0;
for(int j = 0; j <= up; j++)
{
int nt = t && (j == up);
int nl = l && (j == 0);
新状态 = 状态转移逻辑;
if(题目约束不满足) continue;
res += dfs(pos - 1, 新状态, nt, nl);
}
return f[pos][自定义状态][t][l] = res;
}
LL dp(LL n)
{
if(n < 0) return 0;
nums.clear();
while(n)
{
nums.push_back(n % 10);
n /= 10;
}
memset(f, -1, sizeof f);
return dfs(nums.size()-1, 初始状态, 1, 1);
}
int main()
{
LL L, R;
cin >> L >> R;
cout << dp(R) - dp(L - 1) << endl;
return 0;
}
通用模板总结(彻底搞懂)&spm=1001.2101.3001.5002&articleId=163011780&d=1&t=3&u=00435e827bda48a1bd21746f06da4cf9)
1129

被折叠的 条评论
为什么被折叠?



