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} 1≤num1≤num2≤1022
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 0 18 10^{18} 1018),暴力枚举验证会超时。
数位DP的详细讲解可以参考OI Wiki 数位DP
按照上述特征,本题目明显属于数位DP能够解决的问题:
- 本题目要求统计满足 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_sum≤digit_sum(n)≤max_sum的整数数量
- 数位和本身就须需要使用”数位“的思想去理解和解决
- 输入提供了上界和下界
- 上界很大,达到 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<vector<int>>& 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<vector<int>> dp = vector<vector<int>>(num.size(), vector<int>(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;}
};
看完代码实现后,读者可能存在一些疑问:
-
为什么只有在
limit
为false
的情况下才使用缓存dp?设想一种情况,对于
num = 321
,min_sum = 0
,max_sum = 12
的情况:- 当遍历到
23
时,也就是i = 2
,j = 5
,limit = false
,此时记录了dp[2][5]
, - 等继续遍历到
32
时,此时也是i = 2
,j = 5
,但是limit = true
。
这两种情况下,虽然
i
,j
相同,但满足要求的方案数是不同的,前者有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 位重复的数字