数位动态规划(Digit DP)详解与实战

数位动态规划(Digit DP)详解与实战

LeetCode-Py ⛽️「算法通关手册」:超详细的「算法与数据结构」基础讲解教程,从零基础开始学习算法知识,800+ 道「LeetCode 题目」详细解析,200 道「大厂面试热门题目」。 LeetCode-Py 项目地址: https://gitcode.com/gh_mirrors/le/LeetCode-Py

什么是数位动态规划

数位动态规划(Digit Dynamic Programming,简称数位 DP)是一种特殊的动态规划技术,专门用于解决与数字数位相关的计数问题。这类问题通常要求统计在给定区间内满足特定条件的数字个数,或者寻找满足特定条件的第 k 小数字。

数位 DP 的核心思想是将数字按位拆分,然后逐位确定数字的组成,同时利用动态规划的记忆化特性来避免重复计算。

数位 DP 的典型特征

  1. 大范围区间:题目给定的区间往往非常大(如 1 到 10^9),无法通过暴力枚举解决
  2. 数位相关条件:问题的限制条件通常与数字的数位有关
  3. 前缀和思想:通常将区间 [L, R] 的问题转化为 [0, R] 和 [0, L-1] 的差
  4. 逐位确定:从高位到低位依次确定每一位的数字

数位 DP 的实现方法

数位 DP 通常采用记忆化搜索的方式实现,主要考虑以下参数:

  1. 当前位置 pos:当前正在处理的数位位置
  2. 状态 state:记录之前数位的选择情况(如前几位数字的和、已选数字集合等)
  3. 限制标记 isLimit:表示当前位是否受到上界限制
  4. 数字标记 isNum:表示之前是否已经选择了数字(用于处理前导零)

基础模板代码

class Solution:
    def digitDP(self, n: int) -> int:
        s = str(n)
        
        @cache
        def dfs(pos, state, isLimit, isNum):
            if pos == len(s):
                return int(isNum)
            
            ans = 0
            if not isNum:
                ans = dfs(pos + 1, state, False, False)
            
            minX = 0 if isNum else 1
            maxX = int(s[pos]) if isLimit else 9
            
            for x in range(minX, maxX + 1): 
                if (state >> x) & 1 == 0:
                    ans += dfs(pos + 1, state | (1 << x), isLimit and x == maxX, True)
            return ans
    
        return dfs(0, 0, True, False)

经典问题解析

问题1:统计特殊整数

题目描述:统计区间 [1, n] 内所有数位都不相同的数字个数。

解题思路

  1. 将数字转换为字符串,方便逐位处理
  2. 使用状态压缩记录已选数字(用二进制位表示)
  3. 从高位到低位递归处理,考虑是否受限制、是否有前导零等情况

代码实现

class Solution:
    def countSpecialNumbers(self, n: int) -> int:
        s = str(n)
        
        @cache
        def dfs(pos, state, isLimit, isNum):
            if pos == len(s):
                return int(isNum)
            
            ans = 0
            if not isNum:
                ans = dfs(pos + 1, state, False, False)
            
            minX = 0 if isNum else 1
            maxX = int(s[pos]) if isLimit else 9
            
            for x in range(minX, maxX + 1): 
                if (state >> x) & 1 == 0:
                    ans += dfs(pos + 1, state | (1 << x), isLimit and x == maxX, True)
            return ans
    
        return dfs(0, 0, True, False)

问题2:至少有1位重复的数字

题目描述:统计区间 [1, n] 内至少有一位数字重复的数字个数。

解题思路

  1. 正向求解困难,可以反向思考
  2. 先计算所有数位都不相同的数字个数
  3. 然后用总数减去不重复的数字个数

代码实现

class Solution:
    def numDupDigitsAtMostN(self, n: int) -> int:
        s = str(n)
        
        @cache
        def dfs(pos, state, isLimit, isNum):
            if pos == len(s):
                return int(isNum)
            
            ans = 0
            if not isNum:
                ans = dfs(pos + 1, state, False, False)
            
            minX = 0 if isNum else 1
            maxX = int(s[pos]) if isLimit else 9
            
            for d in range(minX, maxX + 1): 
                if (state >> d) & 1 == 0:
                    ans += dfs(pos + 1, state | (1 << d), isLimit and d == maxX, True)
            return ans
    
        return n - dfs(0, 0, True, False)

问题3:数字1的个数

题目描述:计算所有小于等于n的非负整数中数字1出现的个数。

解题思路

  1. 逐位统计数字1出现的次数
  2. 维护一个计数器记录当前已经出现的1的个数
  3. 不需要考虑前导零,简化处理逻辑

代码实现

class Solution:
    def countDigitOne(self, n: int) -> int:
        s = str(n)
        
        @cache
        def dfs(pos, cnt, isLimit):
            if pos == len(s):
                return cnt
            
            ans = 0            
            minX = 0
            maxX = int(s[pos]) if isLimit else 9
            
            for d in range(minX, maxX + 1): 
                ans += dfs(pos + 1, cnt + (d == 1), isLimit and d == maxX)
            return ans
    
        return dfs(0, 0, True)

数位 DP 的优化技巧

  1. 状态压缩:使用二进制位来记录数字出现情况,节省空间
  2. 记忆化剪枝:对于不受限制且已经选择数字的状态,可以直接使用缓存结果
  3. 数学推导:对于某些特定问题,可以结合数学公式减少计算量
  4. 预处理:对于固定长度的数字,可以预处理部分结果

常见问题与解决方案

  1. 前导零处理:通过 isNum 参数区分是否已经开始选择数字
  2. 状态设计:根据问题特点设计合适的状态表示方式
  3. 边界条件:特别注意 n=0 和全9数字的特殊情况
  4. 时间复杂度:合理设计状态,避免状态爆炸

总结

数位 DP 是一种强大的计数工具,特别适合处理大范围内的数字统计问题。掌握数位 DP 的关键在于:

  1. 理解数位 DP 的基本思想和实现模板
  2. 学会根据具体问题设计合适的状态表示
  3. 熟练处理前导零、数位限制等边界条件
  4. 通过大量练习积累经验,提高问题转化能力

通过本文的学习,读者应该能够理解数位 DP 的基本原理,并能够解决中等难度的数位 DP 问题。对于更复杂的问题,需要在掌握基础后进一步学习和实践。

LeetCode-Py ⛽️「算法通关手册」:超详细的「算法与数据结构」基础讲解教程,从零基础开始学习算法知识,800+ 道「LeetCode 题目」详细解析,200 道「大厂面试热门题目」。 LeetCode-Py 项目地址: https://gitcode.com/gh_mirrors/le/LeetCode-Py

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陶真蔷Scott

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值