数位DP专题:经典题 LC233.数字1的个数

最近闲来无事刷刷题,把之前刷题不太熟的东西拿出来重新学习下,写点分享,为以后面试攒攒人品😁


目录

题目介绍

一、浅析数位DP

二、数位DP思路

1.对于大部分数位DP的模板级思维

2.图例

3.预处理

三、代码

总结


题目介绍

tips:本题是力扣水hard,直接上数位DP加一些预处理就能AC


 

一、浅析数位DP

本题很容易想到暴力破解,枚举1~n的所有的数然后找1,但n的范围在【0,10^9】,这么做肯定超时。

换一种思路,由于题面赤裸裸的让你计数,并且告诉你暴力的计数过不了,那么脑海里想到的应该除了用数学分析它😱就是数位DP🤨了,问题来了啥是数位DP呢???

数位是指把一个数字按照个、十、百、千等等一位一位地拆开,关注它每一位上的数字。如果拆的是十进制数,那么每一位数字都是 0~9,其他进制可类比十进制。

数位 DP:用来解决一类特定问题,这种问题比较好辨认,一般具有这几个特征:

  1. 要求统计满足一定条件的数的数量(即,最终目的为计数);

  2. 这些条件经过转化后可以使用「数位」的思想去理解和判断;

  3. 输入会提供一个数字区间(有时也只提供上界)来作为统计的限制;

  4. 上界很大(比如 ),暴力枚举验证会超时。

数位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的递推树,而预处理部分往往是这些题中思考的难点

第一次写分享,如有写的不明白的地方评论区留言,我必及时回复😎

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值