最近闲来无事刷刷题,把之前刷题不太熟的东西拿出来重新学习下,写点分享,为以后面试攒攒人品😁
目录
题目介绍
tips:本题是力扣水hard,直接上数位DP加一些预处理就能AC
一、浅析数位DP
本题很容易想到暴力破解,枚举1~n的所有的数然后找1,但n的范围在【0,10^9】,这么做肯定超时。
换一种思路,由于题面赤裸裸的让你计数,并且告诉你暴力的计数过不了,那么脑海里想到的应该除了用数学分析它😱就是数位DP🤨了,问题来了啥是数位DP呢???
数位是指把一个数字按照个、十、百、千等等一位一位地拆开,关注它每一位上的数字。如果拆的是十进制数,那么每一位数字都是 0~9,其他进制可类比十进制。
数位 DP:用来解决一类特定问题,这种问题比较好辨认,一般具有这几个特征:
要求统计满足一定条件的数的数量(即,最终目的为计数);
这些条件经过转化后可以使用「数位」的思想去理解和判断;
输入会提供一个数字区间(有时也只提供上界)来作为统计的限制;
上界很大(比如 ),暴力枚举验证会超时。
数位dp是一种计数用的dp,一般就是要统计一个区间[l,r]内满足一些条件数的个数。所谓数位dp,字面意思就是在数位上进行dp咯。之所以要引入数位的概念完全就是为了dp。数位dp的实质就是换一种暴力枚举的方式,使得新的枚举方式满足dp的性质,最重要的是这次的dp是对某个数的数位依次进行枚举,然后记忆化就可以了。
上面的介绍比较官方,下面来点接地气的,带着题目来理解数位DP
二、数位DP思路
1.对于大部分数位DP的模板级思维
假设给你一个数x,这个数有n位,我们把最高位的数表示为An 次高位表示为An-1 以此类推 最低位的数表示为A1
步骤1.我们首先统计最高位的An满足题目要求时的答案数,而这时,我们要统计的数的范围通常在【0,T】
说明:T表示一个n位数中最高位为 An -1的最大数,(假如此时 An 为8,n为4,那么T就是7999)
步骤2.当我们统计完当前数位的An时,我们数位下移,跑去统计n-1位上的An-1 统计思路和步骤1一样,不断向下枚举数位当数位枚举到个位时,进入步骤3
步骤3.经过步骤1和步骤2的循环,我们对【0,x】范围中数的只剩下x没有统计过了,此时我们统计x将其产生的部分答案加到结果中即可
假如没接触过数位DP听到这思维可能会很蒙,接下来带入这道题用图举个🌰
2.图例
3.预处理
通过上面的图例应该对数位dp的框架大致上都有了了解,但本题作为一道水hard,思维过程肯定是要有一些的,当你摸清了此时大致的代码框架,知道该如何统计答案时,一个问题呼之欲出了,如何快速的找到一个范围内的1个数
我们的做法如下
步骤一: 设计一个 f[i][j] 数组用于记忆化存储,f[i][j]表示一共有i位数,以 最高位为j的最大数 作为范围的右边界 ,此范围中含1的个数
步骤二:初始化个位数f[1][j]部分
步骤三:从i = 2 开始枚举 考虑f[i][j] 的递推公式
1.当 j == 0 时,代表i位没有数 , 那么 当前f[i][j] 就等于 i-1位时最高位为9的最大数范围中的值即 f[i-1][9]
2.当 j == 1 时,此范围1的个数取决于 i-1位时最高位为9的最大数范围 + 10^(i-1) 【假设当前 i 是 4 表示范围就是[1000,1999]】 + 同位数i中最高位为 j-1时的最大数 范围【假设当前 i 是 4 表示范围就是[0,999]】
3.当 j > 1 时,此范围1的个数取决于 i-1位时最高位为9的最大数范围 【假设当前 i 是 4 表示范围就是[j000,j999]】 + 同位数i中最高位为 j-1时的最大数范围【假设当前 i 是 4 表示范围就是[0,(j-1)999]】
思路大概就是如此,在预处理后使用数位dp的模板套路进行答案汇总即可
三、代码
class Solution {
int[][] f = new int[11][10];//f[i][j]表示一共有i位数,以 最高位为j的最大数 作为范围的右边界 ,此范围中含1的个数
public void init(){
//初始化 个位数
for (int i = 1;i < 10;i ++) f[1][i] = 1;
//从第二位开始更新f数组
for (int i = 2;i < 11;i++){
for (int j = 0;j < 10;j++){
if (j == 0){
f[i][j] = f[i-1][9];
}else {
if (j == 1){
f[i][j] = (int) (f[i-1][9] + Math.pow(10,i-1) + f[i][j-1]);
}else {
f[i][j] = f[i-1][9] + f[i][j-1];
}
}
}
}
}
public int countDigitOne(int max) {
init();
List<Integer> list = new ArrayList<>();
int x = max;
while (x > 0){
list.add(x % 10);
x /= 10;
}
int n = list.size();
int ans = 0;
for (int i = n-1 ; i >= 0;i--){
int cur = list.get(i);
if (cur == 1){
ans += max % Math.pow(10,i);
}
if (cur == 0){
continue;
}else {
ans += f[i+1][cur-1];
}
}
//对分枝中最右叶子节点进行数位统计
for (int i : list){
if (i == 1) ans++;
}
return ans;
}
}
总结
该题比较简单,作为数位dp的入门题,重要的是dp的递推树,而预处理部分往往是这些题中思考的难点
第一次写分享,如有写的不明白的地方评论区留言,我必及时回复😎