模版来源于力扣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]【因题而异,部分复用】
- m 等于字符串数组长度
- n 等于合法方案数的可能最大值
- 记忆数组置默认值,代表未被访问过
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.统计整数数目