文章目录
一、算法概述
- 定义:数位DP(Digit Dynamic Programming)是一种处理数字各位特征的计数问题的算法,通过将数字拆分为各个数位,利用动态规划记录状态,高效统计满足特定条件的数字数量。
- 核心思想:将数字按位拆解,通过递归+记忆化搜索处理每一位的选择,同时跟踪前导零、数位限制等状态,避免重复计算。
- 应用场景:
- 统计区间 [ L , R ] [L, R] [L,R] 中满足各位数字和为 K K K 的数的个数
- 计算二进制中 1 的个数不超过 M M M 的数字数量
- 查找满足数位递增/递减条件的数字
二、算法思路
- 状态定义:
dp[depth][is_leadingZero][is_limit][data0][data1]
:表示处理到第depth
位时,前导零状态、数位限制状态,以及自定义数据data0
和data1
的最优解DpData
结构体:封装数位DP的核心状态数据(如1的个数、各位数位之和 等等)
- 关键状态:
is_leadingZero
:是否为前导零状态(0000就是true
,0002就是false
,所以默认是true
)is_limit
:当前位选择是否受原数字限制(如数字4123中,如果有其中某一位比对应位小,is_limit
就是true
,如果都是正好等于最大的那个数位,就是false
,所以默认是false
)
- 状态转移:
- 对每一位数字,枚举
0
到maxdigit
的可能取值 - 根据前导零和限制状态更新新的状态
- 通过
DpData.getNextDpData
传递状态特征
- 对每一位数字,枚举
- 记忆化搜索:利用
dp
数组存储已计算状态,避免重复计算
三、伪代码实现
1. 数据结构定义
结构体 DpData:
data0: 长整数 # 存储数位特征数据(如1的个数)
data1: 长整数 # 扩展数据位
静态常量 K: 长整数 # 问题参数(如目标1的个数)
静态常量 base: 长整数 # 进制基数(2/10等)
函数 init(): 初始化data0和data1
函数 dfsReturn(is_leadingZero): 返回当前状态是否满足条件
函数 getNextDpData(is_leadingZero, digit): 生成下一位的状态
数组 dp[maxd][2][2][data0_max][data1_max] # 记忆化数组
常量 maxd = 65 # 最大数位长度
2. 核心算法框架
算法:数位DP通用模板
输入:数字n,问题参数K
输出:[1, n]中满足条件的数字个数
函数 DpData.init():
data0 = 0 # 示例:表示二进制中1的个数
data1 = 0 # 预留扩展位
函数 DpData.dfsReturn(is_leadingZero):
if is_leadingZero:
return K == 0 # 前导零情况下的特殊处理
return data0 == K # 示例:判断1的个数是否等于K
函数 DpData.getNextDpData(is_leadingZero, digit):
ret = 当前DpData对象
if 非前导零:
if digit == 1:
ret.data0 += 1 # 示例:二进制中1的个数计数
return ret
函数 dfs(num_str, depth, is_leadingZero, is_limit, dpdata):
if depth == num_str长度:
return dpdata.dfsReturn(is_leadingZero) # 到达末位,返回是否满足条件
# 记忆化检索
ans = dp[depth][is_leadingZero][is_limit][dpdata.data0][dpdata.data1]
if ans != -1:
return ans
ans = 0
maxdigit = if is_limit: base-1 else (num_str[depth] - '0') # 计算当前位最大可选数字
for i from 0 to maxdigit:
# 计算新的前导零状态和限制状态
new_leadingZero = is_leadingZero and (i == 0)
new_limit = is_limit or (i < maxdigit)
# 生成下一位的状态数据
new_dpdata = dpdata.getNextDpData(new_leadingZero, i)
# 递归处理下一位
ans += dfs(num_str, depth+1, new_leadingZero, new_limit, new_dpdata)
dp[depth][is_leadingZero][is_limit][dpdata.data0][dpdata.data1] = ans # 记忆化存储
return ans
函数 getans(n):
初始化dp数组为-1
转换数字n为字符串num_str(按进制base)
创建DpData对象dpd
return dfs(num_str, 0, true, false, dpd)
函数 getans(l, r):
return getans(r) - getans(l-1) # 差分法计算区间[L, R]
四、算法解释
1. 状态初始化
DpData.init()
根据问题需求初始化状态参数(如data0记录1的个数)dp
数组初始化为-1,表示未计算的状态
2. 递归处理流程
- 参数说明:
num_str
:数字的字符串表示(如二进制/十进制)depth
:当前处理的数位位置is_leadingZero
:是否处于前导零状态is_limit
:当前位选择是否受原数字限制dpdata
:当前状态的特征数据
- 终止条件:处理完所有数位,通过
dfsReturn
判断是否满足条件 - 数位枚举:对当前位枚举0到maxdigit的所有可能取值,更新状态并递归
3. 关键状态转移
- 前导零处理:若当前位是前导零(i=0且is_leadingZero为true),则新状态仍为前导零
- 限制状态更新:若当前位选择的数字小于maxdigit,则后续位不再受限制
- 特征数据传递:通过
getNextDpData
更新状态特征(如data0计数1的个数)
4. 示例场景:统计二进制中1的个数等于K的数
- 问题设定:DpData.base=2,DpData.K=K
- 状态定义:data0记录二进制中1的个数
- 转移逻辑:
- 遇到数字1时,data0+=1
- 最终通过dfsReturn判断data0是否等于K
- 输入示例:n=5(二进制101),K=2
- 符合条件的数:3(11)、5(101),共2个
五、复杂度分析
- 时间复杂度:
- 状态数: O ( d × 2 × 2 × d a t a 0 m a x × d a t a 1 m a x ) O(d × 2 × 2 × data0_{max} × data1_{max}) O(d×2×2×data0max×data1max),其中d为最大数位长度(如二进制64位)
- 每个状态计算时间:O(base)(枚举每位数字的可能取值)
- 总时间复杂度: O ( b a s e × d × 2 × 2 × d a t a 0 m a x × d a t a 1 m a x ) O(base × d × 2 × 2 × data0_{max} × data1_{max}) O(base×d×2×2×data0max×data1max)
- 空间复杂度:
- 记忆化数组
dp
: O ( d × 2 × 2 × d a t a 0 m a x × d a t a 1 m a x ) O(d × 2 × 2 × data0_{max} × data1_{max}) O(d×2×2×data0max×data1max) - 递归栈深度:O(d)
- 记忆化数组
- 优化点:
- 状态压缩:若data1未使用,可省略该维度
- 动态规划迭代实现:将递归改为迭代,避免栈溢出
- 适用范围:
- 数字范围:理论上支持任意大整数(受限于字符串转换)
- 时间效率:适用于 10 18 10^{18} 1018 以内的数字计数问题
本文为作者(英雄哪里出来)在抖音的独家课程《英雄C++入门到精通》、《英雄C语言入门到精通》、《英雄Python入门到精通》三个课程的配套文字讲解,如需了解算法视频课程,请移步 作者本人 的抖音直播间。