数字动态规划是处理数位相关计数问题的常用方法,针对指定范围内数字和小于等于X的整数数量统计场景,它通过按位拆分数字、记录中间状态的方式,避免重复计算,大幅提升运行效率。

核心思路拆解
我们需要统计区间[L, R]内所有数字的数位和小于等于X的整数数量,通常可以转化为计算[0, R]的满足条件的数量减去[0, L-1]的满足条件的数量,因此核心是实现计算[0, N]范围内数位和小于等于X的整数数量的函数。
状态定义
定义递归函数dfs(pos, sum, limit, n),各参数含义如下:
- pos:当前处理的数位位置,从最高位开始向下遍历
- sum:当前已经累计的数位和
- limit:布尔值,表示当前位是否受到N的数位限制,如果为true则当前位最大只能取N的对应位数值,否则可以取0-9
- n:传入的上界数字N,用于获取每一位的数值
状态转移逻辑
递归到最低位(pos为-1)时,判断累计数位和sum是否小于等于X,是则返回1,否则返回0。对于每一位,先确定当前位可选取的最大数字:如果limit为true,最大为N的第pos位数值,否则为9。遍历0到该最大值,更新累计数位和和limit状态,递归计算下一位的结果并累加。
记忆化优化
为了避免重复计算,使用三维数组memo[pos][sum][limit]存储已经计算过的结果,其中limit需要转为0和1存储,因为布尔值在数组索引中无法直接作为维度。
完整代码实现
以下是Python语言的完整实现代码:
def count_valid_numbers(n, x):
# 将数字转为字符串方便按位获取
s = str(n)
length = len(s)
# 记忆化数组,limit用0和1表示
memo = [[[-1 for _ in range(2)] for _ in range(x + 1)] for _ in range(length)]
def dfs(pos, sum_val, limit):
# 递归到最低位,判断数位和是否满足条件
if pos == length:
return 1 if sum_val <= x else 0
# 如果已经计算过,直接返回结果
if memo[pos][sum_val][1 if limit else 0] != -1:
return memo[pos][sum_val][1 if limit else 0]
# 确定当前位可选取的最大值
max_digit = int(s[pos]) if limit else 9
res = 0
# 遍历当前位所有可能的数字
for d in range(max_digit + 1):
new_sum = sum_val + d
# 如果新的数位和已经超过x,后续不需要继续计算
if new_sum > x:
continue
new_limit = limit and (d == max_digit)
res += dfs(pos + 1, new_sum, new_limit)
# 存储当前状态的结果
memo[pos][sum_val][1 if limit else 0] = res
return res
return dfs(0, 0, True)
def count_range(l, r, x):
# 计算[l, r]范围内满足条件的数量
if l == 0:
return count_valid_numbers(r, x)
return count_valid_numbers(r, x) - count_valid_numbers(l - 1, x)
# 示例验证
l = 10
r = 50
x = 5
result = count_range(l, r, x)
print(f"区间[{l}, {r}]内数位和小于等于{x}的整数数量为:{result}")
示例说明
以区间[10, 50]、X=5为例,符合条件的数字有10(1+0=1)、11(1+1=2)、12(1+2=3)、13(1+3=4)、14(1+4=5)、20(2+0=2)、21(2+1=3)、22(2+2=4)、23(2+3=5)、30(3+0=3)、31(3+1=4)、32(3+2=5)、40(4+0=4)、41(4+1=5),共14个,运行上述代码可以得到相同结果。
注意事项
- 数位和的阈值X不能设置过大,否则记忆化数组的sum维度会过大,可根据实际场景调整
- 处理左边界为0的场景时,不需要额外减1,避免负数出现
- 如果数字范围超过64位整数,可以先将数字转为字符串再按位处理,避免数值溢出