数位DP

2022/08/19 Algorithms 共 2096 字,约 6 分钟

数位DP

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

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

  1. 要求统计满足一定条件的数的数量(即,最终目的为计数);
  2. 这些条件经过转化后可以使用「数位」的思想去理解和判断;
  3. 输入会提供一个数字区间(有时也只提供上界)来作为统计的限制;
  4. 上界很大(比如 ),暴力枚举验证会超时。

比如统计[a,b] 之间的满足某个条件的数,这个b可能是1e15这样。 注定无法枚举。此类就可以数位DP, 然后a到b形式也可以变成f(b) -f(a)差分来统一处理。记得a这个本身单个可能要额外减去。

模板

  1. 记忆化搜索
  2. 关键参数: 数位i, 用过数字mask, is_limit是否是受限,is_num前面是否填了数字.后面两个参数可适用于其它数位 DP 题目。

该模板对应视频

题目: 2376. 统计特殊整数

如果一个正整数每一个数位都是 互不相同 的,我们称它是 特殊整数 。

给你一个 正 整数 n ,请你返回区间 [1, n] 之间特殊整数的数目。

class Solution:
    def countSpecialNumbers(self, n: int) -> int:
        # 数位dp
        import functools
        '''
        记忆化搜索,i表示计算数位i, mark表示用过的数字
        返回从数位i开始填数字,前面填数字的集合为mask, 能构造出整数的数量
        is_limit表示是否前i-1位是s对应位上的,即最大受限值max了,如果是则当前数字上线是s[i],而不是'9'
        is_num表示前面是否填了数字。若为假,则当前位可以跳过(不填数字),或者要填入的数字至少为 
1;若为真,则要填入的数字可以从 0 开始。
        s表示上限的字符串,恒定不变。比如"13413431431414"
        '''
        @functools.lru_cache(None)
        def f(i: int, mask: int, is_limit: bool, is_num: bool, upper_s: str):
            if i == len(upper_s):
                # 最后一位了,合法则return 1
                return int(is_num)
            res = 0
            #数位i的下限
            low = 0 
            # 数位i的上限
            up = int(upper_s[i]) if is_limit else 9
            
            # When is_num is False: 还没前缀,单独统计
            if not is_num:
                # 此时已经有前缀0了,因此不受限
                res += f(i + 1, mask, False, False, upper_s)
                # 如果是is_num, 则可以从0开始,否则只能从1开始,毕竟是第一位数字。 此时下限只能从1开始
                low = 1

            for d in range(low, up + 1):
                if (mask >> d) & 1 == 0:
                    # 没用过该数字
                    res += f(i + 1, mask | (1 << d), is_limit and d == up, True, upper_s)
            return res

        # 最开始一定是受限的,因此is_limit = True
        return f(0, 0, True, False, str(n))

如果是统计[a,b]的。则可以

        b = f(0, 0, True, False, num2)
        a = f(0, 0, True, False, str(int(num1) - 1))
        return b - a

由于每个状态只会计算一次,因此动态规划的时间复杂度 == 状态个数 ×× 单个状态的计算时间。

时间复杂度为 函数内部复杂度(基本不超过10 毕竟枚举0-9) * 数位数量* mask枚举数量 * 2 * 2. 后面两个是is_limit和is_num,常数可忽略

这里字符串长度为O(logn), log的base为10.

应用

  • 902. 最大为 N 的数字组合 求1-N的使用了digits里数字的数量,没有0, f(i: int, is_limit: bool, is_num: bool)

  • 233. 数字 1 的个数 给定一个整数 n,计算所有小于等于 n 的非负整数中数字 1 出现的个数。def f(i: int, ones: int, is_limit: bool, is_num: bool): ones是当前已用ones的数量。

  • 面试题 17.06. 2出现的次数 和数字1的个数类似。

  • 600. 不含连续1的非负整数 给定一个正整数 n ,返回范围在 [0, n] 都非负整数中,其二进制表示不包含 连续的 1 的个数。def f(i: int, last_is_one: bool, is_limit: bool)

  • 1012. 至少有 1 位重复的数字 等价为n-完全不重复个数

  • 1397. 找到所有好字符串

  • 给你两个长度为 n 的字符串 s1 和 s2 ,以及一个字符串 evil 。请你返回 好字符串 的数目。好字符串 的定义为:它的长度为 n ,字典序大于等于 s1 ,字典序小于等于 s2 ,且不包含 evil 为子字符串。 难点在于要动态维护kmp,先求evil的next数组,然后把匹配j当dp参数传递下去。

参考

https://leetcode.cn/problems/count-special-integers/solution/shu-wei-dp-mo-ban-by-endlesscheng-xtgx/

Search

    Table of Contents