数位动态规划(Digit DP)详解与实战
什么是数位动态规划
数位动态规划(Digit Dynamic Programming,简称数位 DP)是一种特殊的动态规划技术,专门用于解决与数字数位相关的计数问题。这类问题通常要求统计在给定区间内满足特定条件的数字个数,或者寻找满足特定条件的第 k 小数字。
数位 DP 的核心思想是将数字按位拆分,然后逐位确定数字的组成,同时利用动态规划的记忆化特性来避免重复计算。
数位 DP 的典型特征
- 大范围区间:题目给定的区间往往非常大(如 1 到 10^9),无法通过暴力枚举解决
- 数位相关条件:问题的限制条件通常与数字的数位有关
- 前缀和思想:通常将区间 [L, R] 的问题转化为 [0, R] 和 [0, L-1] 的差
- 逐位确定:从高位到低位依次确定每一位的数字
数位 DP 的实现方法
数位 DP 通常采用记忆化搜索的方式实现,主要考虑以下参数:
- 当前位置 pos:当前正在处理的数位位置
- 状态 state:记录之前数位的选择情况(如前几位数字的和、已选数字集合等)
- 限制标记 isLimit:表示当前位是否受到上界限制
- 数字标记 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] 内所有数位都不相同的数字个数。
解题思路:
- 将数字转换为字符串,方便逐位处理
- 使用状态压缩记录已选数字(用二进制位表示)
- 从高位到低位递归处理,考虑是否受限制、是否有前导零等情况
代码实现:
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] 内至少有一位数字重复的数字个数。
解题思路:
- 正向求解困难,可以反向思考
- 先计算所有数位都不相同的数字个数
- 然后用总数减去不重复的数字个数
代码实现:
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的个数
- 不需要考虑前导零,简化处理逻辑
代码实现:
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 的优化技巧
- 状态压缩:使用二进制位来记录数字出现情况,节省空间
- 记忆化剪枝:对于不受限制且已经选择数字的状态,可以直接使用缓存结果
- 数学推导:对于某些特定问题,可以结合数学公式减少计算量
- 预处理:对于固定长度的数字,可以预处理部分结果
常见问题与解决方案
- 前导零处理:通过 isNum 参数区分是否已经开始选择数字
- 状态设计:根据问题特点设计合适的状态表示方式
- 边界条件:特别注意 n=0 和全9数字的特殊情况
- 时间复杂度:合理设计状态,避免状态爆炸
总结
数位 DP 是一种强大的计数工具,特别适合处理大范围内的数字统计问题。掌握数位 DP 的关键在于:
- 理解数位 DP 的基本思想和实现模板
- 学会根据具体问题设计合适的状态表示
- 熟练处理前导零、数位限制等边界条件
- 通过大量练习积累经验,提高问题转化能力
通过本文的学习,读者应该能够理解数位 DP 的基本原理,并能够解决中等难度的数位 DP 问题。对于更复杂的问题,需要在掌握基础后进一步学习和实践。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考