零. 案例引入
1.案例引入 leetcode233. 数字 1 的个数
给定一个整数 n,计算所有小于等于 n 的非负整数中数字 1 出现的个数。
输入:n = 13
输出:6
2.暴力解
对于上述的案例,暴力解肯定是可行的,但时间复杂度较高,对于小于n的所有数字,直接遍历一遍即可
class Solution {
public int countDigitOne(int n) {
int sum = 0;
for(int i = 1; i <= n; i++){
sum += countone(i);
}
return sum;
}
public int countone(int n){
char[] ch = Integer.toString(n).toCharArray();
int sum = 0;
for(int i = 0; i < ch.length; i++){
if(ch[i] == '1'){
sum++;
}
}
return sum;
}
}
3.引出数位DP
显然,上述方法肯定是会超时的,稍微数字大一些就会超时,那么对于数字类按位遍历的题目,可以考虑使用数位DP来解决。由于在以下论述中,会返回出现leetcode233. 数字 1 的个数和leetcode1012 至少有 1 位重复的数字 这两道题目,需要提前看题。
一. 数位DP简单介绍
1.数位DP的本质
这里直接给出数位DP的本质:DFS+记忆化搜索(可选项)+约束(可选项)
那么为了更容易理解,还是以leetcode233. 数字 1 的个数为例,对于原题目,我们很容易得到下图中的疑问与结论(以n=123为例,且先不考虑数字1的个数,只是遍历所有可能结果):
通过观察上图,不难发现:
1. 除了少数情况外,大部分位置都可以取0~9
2. 蓝色情况框定了n可以取值的最大界限
3. 最前方出现的0可以看作不存在(按照题目要求不同,前导0可以不处理)
2.数位DP适合处的问题
(1)数位DP适合处理的典型问题:
Ⅰ. 按位遍历的数字 或者 能组成按位遍历的数字
Ⅱ. 暴力解简单但是数字稍微大一些就超时
Ⅲ. 通常小于某个数的所有合集
(2)为何找规律的方法不建议使用
对于一些题目,找规律进行情况拆解判断当然也是可以的,可以做到更低的时间复杂度和空间复杂度,对于leetcode233. 数字 1 的个数这道题目,极致的找规律方法只需要几行{Ref. [3]}
public int countDigitOne(int n) {
int count = 0;
for (long k = 1; k <= n; k *= 10) {
long r = n / k, m = n % k;
count += (r + 8) / 10 * k + (r % 10 == 1 ? m + 1 : 0);
}
return count;
}
但是在面试笔试中,不管是时间上还是找边界条件和Bug都是不容易的一件事,使用模板更容易解决。
3.数位DP在实际使用面临什么难点
(1) 来到第i个位置,可以取什么数字?
(2)来到第i+1个位置,取值会受到i的影响吗?(本质和第一个是同一个问题)
(3)在某些情况下,不断取值的过程中有可以