算法——数位DP模版

模版来源于力扣2719题,灵茶山艾府写的题解:https://leetcode.cn/problems/count-of-integers/solutions/2296043/shu-wei-dp-tong-yong-mo-ban-pythonjavacg-9tuc/?envType=daily-question&envId=2024-01-16

一、题目描述

给你两个数字字符串 num1 和 num2 ,以及两个整数 max_sum 和 min_sum 。如果一个整数 x 满足以下条件,我们称它是一个好整数:
num1 <= x <= num2
min_sum <= digit_sum(x) <= max_sum.
请你返回好整数的数目。答案可能很大,请返回答案对 10^9 + 7 取余后的结果。
注意,digit_sum(x) 表示 x 各位数字之和。

class Solution {
    public int count(String num1, String num2, int min_sum, int max_sum) {
    }
}

二、数位DP模版1.0

class Solution {
    char s[];
    int memo[][];

    public int numDupDigitsAtMostN(int n) {
        s = Integer.toString(n).toCharArray();
        int m = s.length;
        memo = new int[m][1 << 10];
        for (int i = 0; i < m; i++) 
            Arrays.fill(memo[i], -1); // -1 表示没有计算过
        return n - f(0, 0, true, false);
    }

    int f(int i, int mask, boolean isLimit, boolean isNum) {
        if (i == s.length)
            return isNum ? 1 : 0; // isNum 为 true 表示得到了一个合法数字
        if (!isLimit && isNum && memo[i][mask] != -1)
            return memo[i][mask];
        int res = 0;
        if (!isNum) // 可以跳过当前数位
            res = f(i + 1, mask, false, false);
        int up = isLimit ? s[i] - '0' : 9; // 如果前面填的数字都和 n 的一样,那么这一位至多填数字 s[i](否则就超过 n 啦)
        for (int d = isNum ? 0 : 1; d <= up; ++d) // 枚举要填入的数字 d
            if ((mask >> d & 1) == 0) // d 不在 mask 中
                res += f(i + 1, mask | (1 << d), isLimit && d == up, true);
        if (!isLimit && isNum)
            memo[i][mask] = res;
        return res;
    }
}

作者:灵茶山艾府
链接:https://leetcode.cn/problems/numbers-with-repeated-digits/solutions/1748539/by-endlesscheng-c5vg/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

类中参数和方法中参数理解

类参数

char s[];   // 数位dp允许的最大数字所对应的字符数组
int memo[i][j];   // 记忆化数组,表示数字从左到右构造了i位的情况下的合法方案数为j的数字数量

numDupDigitsAtMostN方法中的参数

int m; // 最大数字的字符串长度

f方法中的参数

// f(i,mask,isLimit,isNum) 表示构造第 i 位及其之后数位的合法方案数
int i;  // 数字从左到右构造第i位
int mask; // 非通用字段,表示前面选过的数字集合,换句话说,第 i 位要选的数字不能在 mask中
boolean isLimit; // 表示当前是否受到了 nnn 的约束。若为真,则第 i 位填入的数字至多为 s[i],否则可以是 9。如果在受到约束的情况下填了 s[i],那么后续填入的数字仍会受到 n 的约束。
boolean isNum; // 表示 i 前面的数位是否填了数字。若为假,则当前位可以跳过(不填数字),或者要填入的数字至少为 1;若为真,则要填入的数字可以从 0 开始。

算法流程个人理解

1. 将上界数字转为字符串数组【复用】

s = Integer.toString(n).toCharArray();

2. 确定二维记忆数组大小meno[m][n]【因题而异,部分复用】

  1. m 等于字符串数组长度
  2. n 等于合法方案数的可能最大值
  3. 记忆数组置默认值,代表未被访问过
int m = s.length;
memo = new int[m][1 << 10];
for (int i = 0; i < m; i++) 
    Arrays.fill(memo[i], -1);

3. 执行dp算法

1. 确定所有数位构造完毕后的结果返回【因题而异】
if (i == s.length)
   return isNum ? 1 : 0;
2. 校验记忆数组是否已经有值,有则直接返回结果,避免重复计算【复用】
if (!isLimit && isNum && memo[i][mask] != -1)
    return memo[i][mask];
3. 遍历数字,确定第i位的值,并进入递归栈(核心)【因题而异、部分复用】
int res = 0;
if (!isNum) // 可以跳过当前数位
    res = f(i + 1, mask, false, false);
int up = isLimit ? s[i] - '0' : 9; // 如果前面填的数字都和 n 的一样,那么这一位至多填数字 s[i](否则就超过 n 啦)
for (int d = isNum ? 0 : 1; d <= up; ++d) // 枚举要填入的数字 d
    if ((mask >> d & 1) == 0) // d 不在 mask 中
        res += f(i + 1, mask | (1 << d), isLimit && d == up, true);
4. 结果保存到记忆数组中【复用】
if (!isLimit && isNum)
   memo[i][mask] = res;

三、解题

思路

题目要求的是num1和num2之间符合好整数要求的数字的数量,可以把问题拆分成:
计算 ≤num2的好整数个数,记作 a。
计算 ≤num1的好整数个数,记作 b。
那么答案就是 a−b。
此时漏掉了num1,对它进行单独判断是否符合好整数的定义,符合则结果为a-b+1

计算≤num的好整数个数,代码逻辑

1. 将上界数字转为字符串数组

char[] s = num.toCharArray();

2. 确定二维记忆数组大小meno[m][n]

int m = s.length;
int n = m * 9; // 此处n代表digit_sum(x)可能的最大值,显然当m位数字每一位都是9时,最大
memo = new int[m][n];
for (int i = 0; i < m; i++) 
    Arrays.fill(memo[i], -1);

3. 执行dp算法

1. 确定所有数位构造完毕后的结果返回
if (i == s.length)
   return mask >= min_sum && mask <= max_sum ? 1 : 0;
2. 校验记忆数组是否已经有值,有则直接返回结果,避免重复计算
if (!isLimit && isNum && memo[i][mask] != -1)
    return memo[i][mask];
3. 遍历数字,确定第i位的值,并进入递归栈(核心)
int res = 0;
int up = isLimit ? s[i] - '0' : 9; // 如果前面填的数字都和 n 的一样,那么这一位至多填数字 s[i](否则就超过 n 啦)
for (int d = isNum ? 0 : 1; d <= up; ++d) // 枚举要填入的数字 d
    res += f(i + 1, mask + d, isLimit && d == up, true);
4. 结果保存到记忆数组中
if (!isLimit && isNum)
   memo[i][mask] = res;

整体代码

class Solution {
    public static int mod = 1000000007;
    char s[];
    int memo[][];
    int min_sum;
    int max_sum;
    public int count(String num1, String num2, int min_sum, int max_sum) {
        this.min_sum = min_sum;
        this.max_sum = max_sum;

        // <=num1的好整数
        s = num1.toCharArray();
        int m = s.length;
        int n = m * 9; 
        memo = new int[m][n];
        for (int i = 0; i < m; i++) 
            Arrays.fill(memo[i], -1);
        int res1 = f(0, 0, true, false);

        // <=num2的好整数
        s = num2.toCharArray();
        m = s.length;
        n = m * 9; 
        memo = new int[m][n];
        for (int i = 0; i < m; i++) 
            Arrays.fill(memo[i], -1);
        int res2 = f(0, 0, true, true);

        // 判断num1是否好整数
        int sum = 0;
        for(int i = 0; i < num1.length(); i++){
            sum += num1.charAt(i) - '0';
        }
        if(sum >= min_sum && sum <= max_sum){
            res1--;
        }
		// 最终的好整数
        int res = res2 - res1 + mod;
        return (int)(res % mod);
    }

    private int f(int i, int mask, boolean isLimit, boolean isNum) {
    	// 最终条件
        if (i == s.length) {
            if(mask >= min_sum && mask <= max_sum){
                return 1;
            }else{
                return 0;
            }
        }
        
        // 记忆数组有值直接返回
        if (!isLimit && isNum && memo[i][mask] != -1){
            return memo[i][mask];
        } 
        
        // 核心逻辑
        int res = 0;
        for (int d = 0, up = isLimit ? s[i] - '0' : 9; d <= up; ++d)
            res = (res + f(i + 1, mask + d, isLimit && d == up, true)) % mod;
        
        // 记忆数组标记
        if (!isLimit && isNum) {
            memo[i][mask] = res;
        }
        return res;
    }
}

本题中,isNum没有相关限制,始终为true,可省略该参数
该方法仅做个人尝试理解使用,力扣上有更好的解法,读者可自行研究
力扣2719.统计整数数目

  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值