求[0,k]内数位和为s的整数个数,寻求更优解法(k极大)
计算[0,k]内数位和等于s的整数个数(大数优化)
问题描述
需要计算范围**[0,k]**内数位和等于s的整数个数,k可能是极大数,因此不能用O(k)的暴力解法。我尝试了O(s log(k))复杂度的思路(log(k)对应k的位数),引入了cnt_sum函数计算n位数(含前导零)中数位和等于s的个数,但遇到了前导零导致的重复统计问题,有没有更简洁的解决办法?
附上我写的未完善伪代码:
# 伪代码,未实现记忆化和边界情况处理 # 计算n位数中数位和等于s的整数个数 # 包含前导零 def cnt_sum(n:int,s:int): ans=0 for i in range(0,9): ans+=cnt_sum(n-1,s-i) return 0 # 假设数字为63069 def dp(loc:int, k:int, s:int): ans=0 # 统计以6开头且剩余数位小于k[1:](3069)、数位和等于sum-k[0](sum-6)的数字 ans+=dp(loc+1,k,s-k[loc]) # 遍历[0,5]中的每个数字i,统计所有长度为len(k)-loc且数位和等于sum-i的数字 # 例如59998、49999 for i in range(0,k[loc]): ans+=cnt_sum(len(k)-loc,s-i) return ans def count(k:int,s:int): dp(0,k,s)
最优解法:带约束的数位DP
你的思路方向是对的,但cnt_sum的实现有问题,且没有处理前导零和边界条件。更简洁可靠的方式是直接用数位DP,通过记忆化搜索处理约束(不超过k的数位),同时自然避免前导零重复问题。
核心思路
数位DP的本质是逐位枚举数字,记录四个关键状态:
- 当前处理到第几位(
pos) - 已选数位的和(
current_sum) - 是否已经小于k的前缀(
tight,布尔值:True表示当前前缀等于k的前缀,后续数位不能超过对应位;False表示前缀已经更小,后续数位可以0-9随意选) - 是否是前导零(
leading_zero,布尔值:True表示前面全是0,当前位可以继续选0,此时不计入数位和)
通过记忆化缓存这四个状态的结果,避免重复计算,时间复杂度为O(位数 × s × 2 × 2),完全满足大数场景。
完整实现代码(Python)
def count_numbers_with_digit_sum(k: str, s: int) -> int: digits = list(map(int, k)) n = len(digits) from functools import lru_cache @lru_cache(maxsize=None) def dp(pos: int, current_sum: int, tight: bool, leading_zero: bool) -> int: # 递归终止:处理完所有数位 if pos == n: # 如果是前导零(即数字0),只有当s=0时算1个,否则0;否则判断当前和是否等于s return 1 if (leading_zero and s == 0) or (not leading_zero and current_sum == s) else 0 limit = digits[pos] if tight else 9 total = 0 for d in range(0, limit + 1): new_tight = tight and (d == limit) new_leading_zero = leading_zero and (d == 0) new_sum = current_sum + (d if not new_leading_zero else 0) # 剪枝:如果当前和已经超过s,没必要继续 if new_sum > s: continue total += dp(pos + 1, new_sum, new_tight, new_leading_zero) return total return dp(0, 0, True, True) # 示例:计算[0,63069]中数位和等于15的个数 print(count_numbers_with_digit_sum("63069", 15))
关键细节说明
- 前导零处理:通过
leading_zero状态标记,前导零阶段的0不会被计入数位和,避免了把"0012"和"12"当成不同数字重复统计的问题。 - 约束控制:
tight状态确保我们只统计不超过k的数字,当tight为True时,当前位的最大值是k对应位的数字,否则可以选0-9。 - 记忆化优化:用
lru_cache缓存状态结果,避免重复计算相同状态的子问题,大幅提升效率。 - 剪枝操作:当当前数位和已经超过s时,直接跳过后续递归,减少不必要的计算。
替代方案:修正你的cnt_sum函数
如果坚持用你最初的思路,需要先修正cnt_sum,并区分"n位数(不含前导零)"和"n位长度的数字(含前导零)":
- 含前导零的n位数字数位和为s的个数:这是典型的整数拆分问题,可用动态规划计算,状态
dp[i][j]表示i位数字(含前导零)数位和为j的个数,转移方程为dp[i][j] = sum(dp[i-1][j-d] for d in 0..9 if j-d >=0) - 不含前导零的n位数(即10(n-1)到10n-1之间的数)数位和为s的个数:等于
cnt_sum(n, s) - cnt_sum(n-1, s)(减去前导零的情况)
但这种方式需要分别计算不同长度的数字,再加上数位长度小于k的情况,不如直接用数位DP简洁。
内容的提问来源于stack exchange,提问作者eigenless
相关产品推荐
相关产品推荐

