数位 DP(记忆化搜索版)通用模板总结(彻底搞懂)

从零基础到独立刷完经典数位 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

  1. memset 位置:必须写在 dp 函数内部,不能只写在 main;换数字、换目标 d 都要重置缓存
  2. 数组越界:初始占位值(如 last=11)不能超过数组维度大小,开数组要留余量
  3. 前导零判断:校验约束用当前层的 l,不是下一层的 nl;前导零状态下约束不生效
  4. 状态传参:递归必须传新计算出的状态,不能误传旧状态
  5. 前缀和负数:取模题目结果必须 (dp(R) - dp(L-1) + mod) % mod,防止负数
  6. 位数限制题:如手机号必须 11 位,不足位数直接返回 0,避免前缀和多减
  7. 输入逆序:部分题目 L 可能大于 R,先执行 swap 再计算
  8. 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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值