【leetcode 2719.统计整数数目】特殊动态规划之数位DP(数位动态规划)

2719. 统计整数数目

题目描述

给你两个数字字符串 num1 和 num2 ,以及两个整数 max_sum 和 min_sum 。如果一个整数 x 满足以下条件,我们称它是一个好整数:

  • num1 <= x <= num2
  • min_sum <= digit_sum(x) <= max_sum.

请你返回好整数的数目。答案可能很大,请返回答案对  1 0 9 + 7 10^9 + 7 109+7 取余后的结果。

注意,digit_sum(x) 表示 x 各位数字之和。

  • 1 ≤ n u m 1 ≤ n u m 2 ≤ 1 0 22 1 \le num_1 \le num_2 \le 10^{22} 1num1num21022
  • 1 <= min_sum <= max_sum <= 400

思路

最直接的思路就是遍历[num1,num2]的所有整数,判断其数位之和是否满足条件。

for(int i = num1;i <= num2;++i) {
    if(digit_sum(i) >= min_sum && digit_sum(i) <= min_sum) {
        ++res;
    }
}

对于一个“困难”题目来说,这个思路无疑太简单了,提交后果然会超时。

数位DP

那么除此之外还有什么解决方案吗?这就要涉及到数位DP了,所谓数位DP:

数位是指把一个数字按照个、十、百、千等等一位一位地拆开,关注它每一位上的数字。如果拆的是十进制数,那么每一位数字都是 0~9,其他进制可类比十进制。

数位 DP:用来解决一类特定问题,这种问题比较好辨认,一般具有这几个特征:

  1. 要求统计满足一定条件的数的数量(即,最终目的为计数);
  2. 这些条件经过转化后可以使用「数位」的思想去理解和判断;
  3. 输入会提供一个数字区间(有时也只提供上界)来作为统计的限制;
  4. 上界很大(比如 1 0 18 10^{18} 1018),暴力枚举验证会超时。

数位DP的详细讲解可以参考OI Wiki 数位DP

按照上述特征,本题目明显属于数位DP能够解决的问题:

  1. 本题目要求统计满足 m i n _ s u m ≤ d i g i t _ s u m ( n ) ≤ m a x _ s u m min\_sum \le digit\_sum(n) \le max\_sum min_sumdigit_sum(n)max_sum的整数数量
  2. 数位和本身就须需要使用”数位“的思想去理解和解决
  3. 输入提供了上界和下界
  4. 上界很大,达到 1 0 22 10^{22} 1022,导致普通思路必然超时。

所以本题目是一种典型的数位DP问题,现在我们可以利用数位DP的方法来解决本题目了。

基于数位DP的解决方案

假设对于一个长度为l的数字num,我们定义f(string num, int i, int j, bool limit)表示构造第i位及其之后数位满足要求的方案数目,另外:

  • j表示前面选中数字(0到i - 1)的数字和。
  • limit,表示当前是否受到了n的约束,因为选择数字时,数字构成的数不能大于n。比如n为123,如果第2位选择2,此时第3位要受到n的限制,只能选择1、2或者3;而如果第2位选择1,此时第3位是不受到限制的,可以选择0-9中的任意数字

那么f(num, 0, 0, true),也就是第0位及其之后数位满足要求的方案数目,也就是区间[0, num]中所有整数中满足数位和大于等于min_sum,并小于等于max_sum的个数。

那么为了求解区间[num1,num2]中所有满足要求的整数个数,只需要f(num2,0,0,true) - f(num1 -1, 0,0,true),也就是最终答案。

在计算过程中,对于f(int i, int j, bool limit)

  • 如果j已经超过了max_sum的限制,表示已经不可能满足要求了,直接返回0。

  • 如果i == l,就是数位0到l-1都已经选定了,也就是已经构造完成,此时j如果满足要求(min_sum <= j <= max_sum),就构成一个方案,返回1.

  • 遍历当前位可能的选择:

    • 如果limit为true,此时需要受到n的限制,也就是只能选择0到n[i](n的第i位)
    • 如果limit为false,可以选择0到9

    对于每一种选择x,可能的方案数是f(i + 1, j + x, limit && x == n[i]),所有可能选择x对应的方案数加起来就是f(i,j,limit)的答案。

为了提升计算效率,我们利用dp[i][j]表示数字填写到第i位,已填的数字位数之和为j时,符合条件的数字个数。

C++代码实现如下:

class Solution {
private:
    const int MOD = 1e9 + 7;
    int max;
    int min;
    int dfs(string num, int i, int j, bool limit,vector>& dp) {
        if(j > this->max) {
            return 0;
        }
        if(i == num.size()) {
            return j >= this->min;
        }
        if(!limit && dp[i][j] != -1){
            return dp[i][j];
        }
        int res = 0;
        int up = limit ? num[i] - '0' : 9;
        for(int d = 0;d <= up;++d) {
            res = (res + dfs(num, i + 1, j + d, limit && d == up, dp)) % MOD;
        }
        if(!limit) {
            dp[i][j] = res;
        }

        return res;
    }
    int get(string num) {
        vector> dp = vector>(num.size(), vector(this->max + 1, -1));
        return dfs(num, 0, 0, true, dp);
    }

    // 获取num - 1对应数字
    string strNumDes(string num) {
        int i = num.size() - 1;
        while (num[i] == '0') {
            i--;
        }
        num[i]--;
        i++;
        while (i < num.size()) {
            num[i] = '9';
            i++;
        }
        return num;
    }
public:
    int count(string num1, string num2, int min_sum, int max_sum) {
        this->max = max_sum;
        this->min = min_sum;
        return (get(num2) - get(strNumDes(num1)) + MOD) % MOD;
    }
};

看完代码实现后,读者可能存在一些疑问:

  • 为什么只有在limitfalse的情况下才使用缓存dp?

    设想一种情况,对于num = 321min_sum = 0max_sum = 12的情况:

    • 当遍历到23时,也就是i = 2, j = 5limit = false,此时记录了dp[2][5]
    • 等继续遍历到32时,此时也是i = 2j = 5,但是limit = true

    这两种情况下,虽然ij相同,但满足要求的方案数是不同的,前者有8种(230、231、232、233、234、235、236、237),后者受限于数字num,只有2种(320、321)。

    因此,只有在limit为false时才使用缓存。

  • 基于上面那个问题,那如果将limit加入到缓存维度中,是否就可以不再特殊考虑limit了?比如dp[2][i][j],其中dp[0][i][j]表示limit为false的情况,dp[1][i][j]表示limit为true的情况。

    这种方案当然可以得到最终答案,但在这么做之前可以先思考一下是否有必要。
    limit为true的子问题只会被调用一次,将对这些子问题做缓存并不会提升性能,所以并没有必要这么做。

其他数位DP问题

数位DP相关的题目有很多,但只要了解其中规律,应该还是可以顺利解决的,罗列下面几个数位DP相关题目,供大家训练加强。

  • 233. 数字 1 的个数
  • 面试题 17.06. 2出现的次数
  • 600. 不含连续1的非负整数
  • 902. 最大为 N 的数字组合
  • 1067. 范围内的数字计数
  • 1012. 至少有 1 位重复的数字

你可能感兴趣的:(LeetCode,leetcode,动态规划,算法,数位DP,c++)