首页 > 其他分享 >数位 dp 学习笔记(灵神模板)

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

时间:2024-01-26 23:16:34浏览次数:34  
标签:灵神 lead int text s1 high 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$ 位填数字,数字时是从最高位开始依次往低位填的。$\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);
    }
};

  [SCOI2009] windy 数

  这题很明显不能有前导零,因为前导零会影响到相邻两个数字之差至少为 $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;
}

  E - Digit Sum Divisible

  直接 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

相关文章

  • logback.xml配置文件模板
    1<?xmlversion="1.0"encoding="UTF-8"?>2<configuration>3<!--4CONSOLE:表示当前的日志信息是可以输出到控制台的。5-->6<appendername="CONSOLE"class="ch.qos.logback.core.ConsoleAppender......
  • edusrc—freemarker模板注入RCE
    freemarker简述FreeMarker是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件信息收集开局一个登入框弱口令admin/123456登入......
  • POJ--3616 Milking Time(DP)
    记录19:522024-1-26http://poj.org/problem?id=3616reference:《挑战程序设计竞赛(第2版)》第二章练习题索引p135一个LIS(最长上升子序列,LongestIncreasingSubsequence)问题的变种dp[i]表示第i个interval结尾能获得最多的milk,首先需要把数据按照起始时间排序,第i个表示......
  • 软件验收测试报告模板
    ......
  • 软件验收测试计划模板
    ......
  • 产品打包清单模板
    ......
  • 产品交接验收单模板
    ......
  • 软件产品验收报告模板
    ......
  • 动态规划dp-背包问题
    https://www.luogu.com.cn/problem/P1048?contestId=154692`include<bits/stdc++.h>usingnamespacestd;intv[105];intvalue[105];intdp[105][1005];intmain(){intt,m;cin>>t>>m;for(inti=1;i<=m;i++){cin>>v[i]>>......
  • 拓扑排序模板
    给定一个DAG(有向无环图),如果从\(u\)到\(v\)有边,则认为\(v\)依赖于\(u\)。如果\(u\)到\(v\)有路径(\(u\)可达\(v\)),则称\(v\)间接依赖于\(u\)。我们将图中的顶点以线性方式进行排序,使得对于任何的顶点\(u\)到\(v\)的有向边\((u,v)\),都可以有\(u\)在\(v\)的......