Leetcode Link: 233. 数字 1 的个数 - 力扣(LeetCode) 面试题 17.06. 2出现的次数 - 力扣(LeetCode)

题目

数位 dp

数位 dp 解决的是在某一个范围[L, R]上满足某种条件的数字的个数,一般 R 或者 L 或者范围都非常大,直接使用暴力枚举一定超时。

而数位 dp 则是将数字视为对应的字符串,通过逐位放置合法的数字来不断求解答案。从暴力枚举的思路上看,数位 DP 属于从左到右的尝试模型,但是要有更多,更复杂的状态,这要根据题目具体分析。

常见的状态是is_boundis_zero

举例 想知道小于 3135 的数字的中,有多少数字的相邻位数大于等于2。

我们从左到右放置,假设放置到了第 2 位:

  • 如果放置形如 13_ _,此时可以放置的数字范围是[0, 9]
  • 如果放置形如 31_ _,此时允许的范围是[0, 3] 显然,我们需要有一个布尔变量指示当前位能够放置的数字的最大值。当前面位置的数字都是贴着放的时候,当前只能放置[0, num[i]],反之,能放置[0, 9]

有时,前导 0 也会影响我们的某些判定,本例子中,如果前面都是前导0,则当前位是首位,是可以放置1的。因此,需要使用is_zero来指示我们前面是否已经不包含前导0了。

解法一

思路: 本题即采用数位 dp 的思路。

查找记忆的时候需要注意,当 is_bound=True 的时候,我们的 maxi 的范围是收到 num[i] 决定的,即是特殊的,因此不能再 memo 里面去直接取。

题解

class Solution:
    def countDigitOne(self, n: int) -> int:
        s = str(n) # 先转换成字符串
        memo = {}  # 记忆化搜索
        def process(i: int, cnt: int, is_bound: bool):
        '''
	        i: 当前遍历的位置
	        cnt: 之前的所有位置上含有数字1的个数
	        is_bound: 是否贴着上边界放置数字
        '''
	        # base case: 当遍历到越界,则返回之前所有含有的1的个数
            if i == len(s):
                return cnt
            # 只有当不是贴边放的时候,才能尝试直接取
            if not is_bound and (i, cnt) in memo.keys():
                return memo[(i, cnt)]
 
            maxi = int(s[i]) if is_bound else 9
            res = 0
            for val in range(maxi+1):
                next_cnt = cnt+1 if val == 1 else cnt
                res += process(i+1, next_cnt, is_bound and val==maxi)
            
            if not is_bound: memo[(i, cnt)] = res
            return res
        
        return process(0, 0, True)

启发和联系

本题不需要 is_zero,前导 0 的判定对放置没有影响。

由于里面设计到不好int到str和str到int的转换,尽量明晰哪个位置需要进行转换,不要乱了。