LeetCode No.233 Number of Digit One 题解

LeetCode No.233 Number of Digit One 题解

题目描述


Given an integer n, count the total number of digit 1 appearing in all non-negative integers less than or equal to n.

For example:
Given n = 13,
Return 6, because digit 1 occurred in the following numbers: 1, 10, 11, 12, 13.


题目分析

这题这形式给出来就直接让人想到数位DP。。果断试着推了一下, 发现不是很难。 尤其是这题只要讨论数字1出现的次数, 下面推导的过程我们可以看到这比其他数字更加容易推导。

由于这是第一篇数位DP的博客。 就先简单介绍一下数位DP是什么。
先从DP开始说起。Dynamic Programming 我的简单理解是,将原问题分解为相同性质的子问题 (这一步分解很重要!). 然后需要的就是推导出子问题与原问题之间的联系(如果把不同规模的问题,看作是相同问题的不同状态, 这这个联系的函数表达即是常说的状态转移方程)
数位DP其实就是DP的一种形式罢了。有区别与一般的DP在于, 对于一般的数n,状态的转移一般是在同一个数量级上的转移。不负责任的写一个常用的一维DP形式:
f[k] = func(f[k - 1], f[k - 2], ... f[k - n])

这个式子里面, func函数是实现状态转移的主体, 视不同情况而不同。 需要注意的是这里func的参数是该问题的前n个状态。 这与当前状态处于同一个数量级上。
而数位DP则显得更加“效率”, 它的状态转移是从 logN 转移上来的。比如说直接从f[10]转移到f[100],再到f[1000]之类。 这类DP的状态转移是在数位上进行的。换个角度去看, 即是从状态2(2位)转移至状态3(3位), 再到状态4.(4位)

这道题的数位DP形式是特别典型的那种。所以就先闲扯了一点。 下面来看这题本身。由于这是裸数位DP, 所以思路什么的就略去了。下面直接开始推最关键的, 状态转移方程。
要推方程还得先看问题要怎么分解为子问题。来举个栗子:n = 2101.
如果稍微能类比一下的话, 套用上面写的一维DP的形式, 很容易猜想到可能方程会以如下形式出现:
f[2101] = func(f[1000], f[100], f[10], f[1])
这就是在数位上而言, 的前几个状态嘛~
这里我们也可以看到, 数位DP可以用非常少的状态来得到我们需要的下一个状态。 这侧面反映了我们如果迭代去计算时必然包含了大量的重复运算。重复在哪里? 以上面那个例子,来具体列举几个:
f[2000] = f[1000] + f[2000] - f[1000]
这样写的目的,就是把2000分成两部分去看。然后对应上, 是不是发现, 除了最高位, 其他都是相等的1的个数呢。。。
001,002...100...998, 999
1001,1002...1100...1998, 1999

所以可以看到, 我们只要算出了f[1000], 事实上f[2000]是可以直接推出的, 有f[2000] = 2 * f[1000] + 1000
在这里我要特别说明一下, 这里的f[1000]是不包含1000本身的,计算的是从1~999中1的个数。这样做的目的就是为了使状态转移方程好看一些。
类似的, 我们应该能够推出,f[2101] = 2 * f[1000] + 1000 + f[100] + f[1]

推到这一步, 我们做到什么程度了呢? 就是把任意一个数n的f[n]分解为了几个 10n 的状态。(再提醒一句, 在数位DP中, 这些就是连续状态)换个表述来说, 我们可以把原问题的任意状态, 表示为一系列“基状态”的线性组合(对于一般的DP, 这里应该写组合, 而这题而言,显然是线性的)。下面需要推导的,就是基状态之间如何转移的。

实际上呢。。。基状态的转移和上面的, 把原问题分为一系列基状态的过程, 十分类似。 考虑f[10]->f[100]:

f[100] = f[100] - f[90] + f[90] - f[80]...- f[10] + f[10]

拆分出来的10个部分, 每一部分的值与 f[10]都有紧密联系。除了 10~19这一部分, 其他的9部分值是和 f[10]相同的。
讲了这么多, 我觉得大概直接贴状态转移应该也能懂了吧。。
f[i] => 1 ~ (10 ^ i - 1) 中 1 的出现次数
f[i] = 10 * f[i - 1] + num(i - 1), num(i - 1)为 pow(10, i - 1)

到这里, 基本就解释完了整个过程。其实不算复杂。 只是我表达能力欠缺。orz。

一行代码胜千言, 下面是实现代码:


class Solution {
public:
    int countDigitOne(int n) {
        if (n < 1) return 0;
        int i = 0, dealt = 0, ans = 0;
        int f[20];
        memset(f, 0, sizeof(f));
        // f[i] => 1 ~ (10 ^ i - 1) 中 1 的出现次数
        // f[i] = 10 * f[i - 1] + num(i - 1), num(i - 1)为 pow(10, i - 1)
        // dealt: 已经处理完的数字(n的某个右侧部分)
        while (n > 0) {
            int t = n % 10;
            n/= 10;
            f[i] = i * pow(10, i - 1);
            if (t == 1) ans+= dealt + 1 + f[i];
            else if (t > 1) ans+= t * f[i] + pow(10, i);
            dealt+= t * pow(10, i);
            i++;
        }
        return ans;
    }
};

总代码量就22行,还包含了三行注释。。。前文看不懂的直接看代码算了。。。

其他细节

  • 由于我想把这篇博客的重点放在讲解数位DP上,因此有很多实现细节没有说明清楚。 不过略看一下代码应该很清楚的。
  • 实际上当数位为1时, 需要另外讨论。 这在代码中也有体现。 正是这一点, 使得计算1的个数比其他数字来的稍微容易一点: 1的话只要讨论>1和=1的情况。其他数字要讨论3种(0除外)
  • 数位DP博大精深,这题只是比较水的一题而已, 我自己对数位DP的题也没多少自信(事实上DP我都很虚)。。。如果上面的有说错的地方,希望大家不吝赐教。

The End.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值