数位 dp 学习笔记(灵神模板)

  我谔谔,数位 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 位填数字,数字是从最高位开始依次往低位填的。high 是一个 bool 变量,如果是 1 表示前面填的数字都是 n 对应位上的数字,那么第 u 位能填的数字只能是 [0,s[u]];否则如果是 0 表示前面至少存在一个位填的数字小于 n 对应位上的数字,那么第 u 位能填的数字可以是 [0,9]

  dfs(u, high) 返回的是从第 u 位开始填,第 u 位前面填的数字是否都贴着上界,所能构造出满足条件的数的数量。边界条件是 u 等于 n 的数位长度,此时返回 1(有其他条件的话还要判断是否满足)。

  lr 对应第 u 位上能填的数字的范围,其中在这个模板中 l 都是 0rhigh 的影响。

  f(u) 是为了实现记忆化搜索,其实可以把 f 扩展成 f(u,0/1),实现时之所以不用记录 high 那维,是因为当 high=1 时必然只会搜一次(标记,这里其实我现在也不是很理解)。

  最后调用的方法是 dfs(0, 1),一开始置 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;
}

  参数中的 lead 是一个 bool 变量,如果是 1 表示第 u 位之前都没有填过任何数字,表示有前导零;否则如果是 0 表示第 u 位之前填过数字,没有前导零。当递归到边界时,如果 lead=1 表示都没填过数字,返回 0,否则返回 1

  当 lead=1 时,当前第 u 位可以继续不填数字,即执行 dfs(u + 1, 0, 1)high 会变成 0,是因为前 u 位都没填过数字,自然就不会帖着 n 对应位的上界。

  另外第 u 为最小能填的数取决于 lead,当 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, 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;
}

  当询问的 LR 超过 long long 的范围时,我们需要转成字符串的形式去求 dp(R) - dp(L-1),这里 L1 就要涉及到高精度减法了,使用这个模板有一个好处就是可以避免这个减 1 的处理。

  代码中的 s1s2 分别对应 LR 转换成字符串的结果。如果 s1 的长度小于 s2 的长度,那么在 s1 前补充 0 使其与 s2 的长度相等。

  参数中的 lowhigh 类似,用来表示第 u 位前填的数字是否都贴着 L 的下界。

  对应的 l 的取值就会受到 low 的影响,当 low=1 说明第 u 位只能从 s1[u] 开始填(否则填的数字就会小于 L),否则可以从 0 开始填。当考虑前导 0 时,那么可以填的最小数字理应是 min{l,lead}。需要注意的是不可以令 l=max{l,lead},这里的 lr 是第 u 位可以填的数字的下界和上界,只能受到 lowhigh 的影响,lr 不能被修改

  调用的方法是 dfs(0, 1, 1) 或 dfs(0, 1, 1, 1)

  下面选一些题来应用这几个模板。


  统计强大整数的数目

  首先考虑数字能否有前导零,虽然题目说的是统计不含前导零的数的数量,但可以发现是否有前导零并不影响结果,这是因为对于满足最低的 s.size() 位与 s 一样,且每一位的数字不超过 limit 的数,很明显补上前导零后这个数还是满足这两个条件。

  当 u 不在最低的 s.size() 位时,那么第 u 位可以填的数字是 [l,min{r,limit}],注意不能把 rlimit 取最小值。否则 u 在最低的 s.size() 位时,那么第 u 位只能填 s 对应位上的数字 t = s[s.size() - (s1.size() - u)] - '0',并且只有 t 满足 ltr 时才能填。

  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);
    }
};

  [SCOI2009] windy 数

  这题很明显不能有前导零,因为前导零会影响到相邻两个数字之差至少为 2 这个条件。同时我们还要用一个参数记录上一个数字选了什么,记作 last。最后在枚举当前可以填的数字 i 时,只能选满足 |ilast|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;
}

  E - Digit Sum Divisible

  直接 dp 的话发现做不了。考虑把 1N 中的数按照每位数字之和进行分类,那么最多可以分成 9×14 类。然后枚举各位数字之和 p,分别求 1N 中有多少个数的各位数字之和为 p 且这个数模 p 等于 0。另外可以发现前导零不会影响答案,dp 的参数列表为 dfs(u, s, r, p, high),其中 s 表示前 u 位的数字之和,r 表示前 u 位构成的十进制数模 p 的值。

  答案就是 p=19×14dfs(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/

posted @   onlyblues  阅读(772)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
Web Analytics
点击右上角即可分享
微信分享提示