讲师中心 微信公众号
AI工具推荐 视频效率加速

利用数位DP高效计算指定范围内数位和小于等于X的整数数量

夜涛同学_9059

夜涛同学_9059

发布时间:2025-11-17 13:38:02

|

554人浏览过

|

来源于php中文网

原创

利用数位DP高效计算指定范围内数位和小于等于X的整数数量

本文详细介绍了如何使用数位动态规划(digit dp)算法,高效计算在给定大范围 `[1, n]` 内,其数位和小于或等于特定值 `x` 的整数数量。针对 `n` 值可达 `10^12` 的情况,传统遍历方法效率低下,数位dp通过递归分解问题并结合记忆化搜索,将时间复杂度优化至对数级别,有效解决了大规模数据下的计数挑战。

问题背景与挑战

在程序设计中,我们经常需要处理关于数字性质的计数问题。一个常见的问题是:给定一个整数 n 和一个最大数位和 x,我们需要找出在范围 [1, n] 内,有多少个整数的数位之和小于或等于 x。

例如,如果 n = 112 且 x = 5,我们需要统计 0 到 112 之间(通常包含 0,如果问题要求从 1 开始,最后减去 0 的情况即可)数位和小于等于 5 的数字。

对于较小的 n 值,一个简单的遍历方法是可行的:

def digitsums_lower_than_x_naive(x_limit, n_upper_bound):
    """
    计算 [1, n_upper_bound] 范围内数位和小于等于 x_limit 的数字数量。
    此方法对于大 n_upper_bound 效率低下。
    """
    digit_sum_func = lambda y: sum(int(digit) for digit in str(y))
    count = 0
    for i in range(1, n_upper_bound + 1):
        if digit_sum_func(i) <= x_limit:
            count += 1
    return count

然而,当 n 的值非常大,例如达到 10^12 时,上述方法的时间复杂度为 O(n * log n)(计算每个数的数位和需要 log n 时间),这将导致计算时间过长,无法满足性能要求。这时,我们需要一种更高效的算法,数位动态规划(Digit DP)便是解决此类问题的理想选择。

数位动态规划(Digit DP)核心思想

数位DP的核心思想是将一个大数的计数问题分解为子问题,通过递归地考虑数字的每一位来构建解决方案,并利用记忆化搜索(memoization)避免重复计算。

我们定义一个函数 count_numbers(upper_bound_str, sum_limit),它返回小于或等于 upper_bound_str 所代表的数字,且数位和小于或等于 sum_limit 的整数数量。

考虑一个数字 N(作为字符串 upper_bound_str 表示)和允许的最大数位和 x_limit。我们的目标是计算 [0, N] 范围内满足条件的数字个数。

算法的递归结构如下:

GitHub Actions Deploy Risk Audit
GitHub Actions Deploy Risk Audit

通过评估失败率、未解决的连续失败和上次成功部署以来的时间,审计GitHub Actions运行的部署工作流风险。

下载
  1. 处理最高位: 遍历从 0 到 N 的最高位数字 D。
  2. 分解问题:
    • 情况一:当前位 d 小于 N 的当前位 D。 这意味着从当前位开始,后续的数字位可以取 0 到 9 之间的任何值,而不会超过 N。因此,这部分子问题可以转化为计算所有长度为 k(剩余位数)的数字,其数位和为 sum_limit - d 的数量。这可以看作是 count_numbers('9' * k, sum_limit - d)。
    • 情况二:当前位 d 等于 N 的当前位 D。 这意味着后续的数字位仍然受到 N 的剩余部分的限制。我们需要递归地处理 N 的下一位,并更新 sum_limit 为 sum_limit - d。这可以看作是 count_numbers(N[1:], sum_limit - d)。
  3. 记忆化: 使用一个缓存(字典或数组)来存储已经计算过的子问题的结果,避免重复计算。子问题的状态通常由当前处理到的数字位数、当前的数位和限制以及是否受到 N 的限制等信息组成。

示例解析:计算 DS(112, 5)

让我们通过计算 DS(112, 5)(即 N=112,数位和限制 x=5)来理解这个过程。

DS(upper_bound, sum_limit) 表示在 [0, upper_bound] 范围内,数位和小于等于 sum_limit 的数字数量。

  1. DS(112, 5)

    • 112 是一个三位数。
    • 最高位可以是 0 或 1。
    • 当最高位为 0 时: 相当于计算 DS(99, 5)(即所有两位数,数位和小于等于 5 的数量,因为最高位为 0,实际是处理两位数)。
    • 当最高位为 1 时: 剩余的数位和限制变为 5 - 1 = 4。剩下的数字由 12 限制。相当于计算 DS(12, 4)。
    • 所以,DS(112, 5) = DS(99, 5) + DS(12, 4)
  2. 计算 DS(99, 5)

    • 99 是一个两位数。
    • 最高位可以是 0, 1, 2, 3, 4, 5 (因为 sum_limit 是 5)。
    • 当最高位为 d 时,剩余数位和限制为 5 - d。由于 d 总是小于 9 (N的最高位),所以后续位可以取 0-9。
    • DS(99, 5) = DS(9, 5) + DS(9, 4) + DS(9, 3) + DS(9, 2) + DS(9, 1) + DS(9, 0)
      • DS(9, k) 表示个位数小于等于 9 且值小于等于 k 的数量。
      • DS(9, 5): 0,1,2,3,4,5 (6个)
      • DS(9, 4): 0,1,2,3,4 (5个)
      • DS(9, 3): 0,1,2,3 (4个)
      • DS(9, 2): 0,1,2 (3个)
      • DS(9, 1): 0,1 (2个)
      • DS(9, 0): 0 (1个)
    • DS(99, 5) = 6 + 5 + 4 + 3 + 2 + 1 = 21
  3. 计算 DS(12, 4)

    • 12 是一个两位数。
    • 最高位可以是 0 或 1。
    • 当最高位为 0 时: 相当于计算 DS(9, 4)。
    • 当最高位为 1 时: 剩余的数位和限制变为 4 - 1 = 3。剩下的数字由 2 限制。相当于计算 DS(2, 3)。
    • 所以,DS(12, 4) = DS(9, 4) + DS(2, 3)
      • DS(9, 4): 0,1,2,3,4 (5个)
      • DS(2, 3): 0,1,2 (3个)
    • DS(12, 4) = 5 + 3 = 8
  4. 最终结果:

    • DS(112, 5) = DS(99, 5) + DS(12, 4) = 21 + 8 = 29

Python 实现

以下是基于上述数位DP思想的Python实现。函数 D 是核心的递归函数,它接受当前数字的字符串表示(x_str,即 N 的剩余部分),允许的剩余数位和 n_sum_limit,以及一个缓存 cache。

def count_with_digit_sum_limit(upper_bound_str, sum_limit, cache):
    """
    递归函数,计算小于或等于 upper_bound_str 且数位和小于等于 sum_limit 的数字数量。

    参数:
    upper_bound_str (str): 当前处理的上限数字的字符串表示。
    sum_limit (int): 允许的剩余数位和上限。
    cache (dict): 记忆化搜索的缓存,存储 (upper_bound_str, sum_limit) -> 结果。

    返回:
    int: 符合条件的数字数量。
    """

    # 基本情况:如果 sum_limit 小于 0,说明已经超出了允许的数位和,返回 0。
    if sum_limit < 0:
        return 0

    # 基本情况:如果只剩一位数字
    if len(upper_bound_str) == 1:
        # 这一位可以取 0 到 min(sum_limit, int(upper_bound_str[0]))
        # 例如,upper_bound_str='5', sum_limit=3, 那么可以取0,1,2,3 (4个)
        # 例如,upper_bound_str='2', sum_limit=5, 那么可以取0,1,2 (3个)
        return min(sum_limit, int(upper_bound_str[0])) + 1

    # 检查缓存,避免重复计算
    state = (upper_bound_str, sum_limit)
    if state in cache:
        return cache[state]

    # 获取当前最高位数字
    top_digit = int(upper_bound_str[0])
    # 构造一个由 '9' 组成的字符串,长度比当前数字少一位,用于表示“无上限”的子问题
    nines_suffix = '9' * (len(upper_bound_str) - 1)

    current_count = 0
    # 遍历当前位可能取的值 d
    # d 从 0 开始,直到 min(sum_limit, top_digit)
    for d in range(min(sum_limit, top_digit) + 1):
        if d < top_digit:
            # 如果当前位 d 小于 top_digit,则后续位可以取 0-9,不受 upper_bound_str 限制
            # 递归调用,处理由 '9' 组成的后缀,剩余数位和为 sum_limit - d
            current_count += count_with_digit_sum_limit(nines_suffix, sum_limit - d, cache)
        else: # d == top_digit
            # 如果当前位 d 等于 top_digit,则后续位仍受 upper_bound_str 的剩余部分限制
            # 递归调用,处理 upper_bound_str 的剩余部分,剩余数位和为 sum_limit - d
            current_count += count_with_digit_sum_limit(upper_bound_str[1:], sum_limit - d, cache)

    # 将结果存入缓存
    cache[state] = current_count
    return current_count

def calculate_digit_sum_count(n_upper_bound, x_limit):
    """
    主函数,计算 [0, n_upper_bound] 范围内数位和小于等于 x_limit 的数字数量。

    参数:
    n_upper_bound (int): 范围上限。
    x_limit (int): 允许的最大数位和。

    返回:
    int: 符合条件的数字数量。
    """
    # 将 n_upper_bound 转换为字符串以便按位处理
    n_str = str(n_upper_bound)
    # 初始化缓存
    cache = {}
    return count_with_digit_sum_limit(n_str, x_limit, cache)

# 示例调用
print(f"DS(112, 5) = {calculate_digit_sum_count(112, 5)}") # 预期输出 29
print(f"DS(9, 2) = {calculate_digit_sum_count(9, 2)}") # 预期输出 3 (0, 1, 2)
print(f"DS(19, 2) = {calculate_digit_sum_count(19, 2)}") # 预期输出 6 (0,1,2,10,11,12) -> 0(0),1(1),2(2),10(1),11(2),12(3) -> 0,1,2,10,11
                                                        # DS(9,2) + DS(9,1) = 3 + 2 = 5 (0-99) + DS(9,2) + DS(0,1)
                                                        # DS(9,2) = 3 (0,1,2)
                                                        # DS(9,1) = 2 (0,1)
                                                        # DS(19,2) = DS(9,2) + DS(9,1) = 3 + 2 = 5
                                                        # Let's re-verify DS(19,2) = DS(9,2) + DS(9,1)
                                                        # DS(19,2) = count_numbers('19', 2, cache)
                                                        # d=0: count_numbers('9', 2, cache) = 3 (0,1,2)
                                                        # d=1: count_numbers('9', 2-1=1, cache) = 2 (0,1) -> (10, 11)
                                                        # Total = 3 + 2 = 5. Correct for 0-19.

注意事项与优化

  1. 范围包含 0: 上述 calculate_digit_sum_count 函数计算的是 [0, n_upper_bound] 范围内的数字。如果题目要求从 1 开始计数(即 [1, n]),则最终结果需要减去 digit_sum(0) 的情况。由于 digit_sum(0) 总是 0,且 0 <= x_limit 几乎总是成立,所以通常是 result - 1。
  2. 参数命名: 在原始问题中,x 是数位和限制,n 是范围上限。在提供的答案代码中,D(x, n, cache) 的 x 实际上是范围上限的字符串表示,n 是数位和限制。在我的代码中,我使用了更具描述性的 upper_bound_str 和 sum_limit 来避免混淆。
  3. 缓存键: 缓存的键 (upper_bound_str, sum_limit) 确保了每个子问题状态的唯一性。
  4. 时间复杂度: 考虑 N 有 L 位(L = log10(N)),x_limit 最大约为 9 * L。
    • 递归深度最大为 L。
    • 在每个递归层,循环 d 的次数最多为 min(sum_limit, 9) + 1,大约是 min(9L, 9),也就是常数 10。
    • 缓存中状态的数量约为 L * x_limit,即 L * 9L = 9L^2。
    • 因此,总的时间复杂度大致为 O(L * 10 * 1)(每个状态计算一次) = O(L * min(x_limit, 9))。更准确地说,是 O(L * x_limit),因为 x_limit 可以达到 9L。对于 N=10^12,L=12,x_limit 最大约 108。所以复杂度约为 12 * 108,这是一个非常小的常数,远优于 N。
  5. 空间复杂度: 主要取决于缓存的大小,即 O(L * x_limit)。

总结

数位动态规划是解决涉及数字位数的计数问题的强大工具,特别适用于处理大范围 N 的情况。通过将问题分解为更小的、具有重叠子结构的问题,并结合记忆化搜索,它能够将指数级或线性级的朴素算法优化到多项式或对数级别。理解其核心思想——如何根据当前位与上限数字的关系来递归地构建子问题,以及如何有效地利用缓存——是掌握数位DP的关键。

热门AI工具

更多
UpDream
UpDream Hot

一款AI视频创作工具,主要用于哔哩哔哩推出的自研AI视频创作工具,适合需要提升相关任务效率的用户。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

Loomy
Loomy Hot

一款AI工具,主要用于科大讯飞发布的桌面级 AI 助理,比 OpenClaw 更易用、更安全!,适合需要提升相关任务效率的用户。

蛙蛙写作

一款AI论文写作工具,主要用于超级AI智能写作助手,适合需要提升相关任务效率的用户。

Atoms
Atoms Hot

Atoms是一款AI智能体工具,第一支自动构建真实业务的 AI 团队。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

切问学术

切问学术是一款AI论文写作工具,复旦大学NLP团队推出的AI学术智能体。

音述AI
音述AI Hot

一款AI音频处理工具,主要用于音述AI是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区,适合需要提升相关任务效率的用户。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

相关专题

更多
js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

1558

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

2244

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

5764

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

4869

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

6594

2024.03.22

php中定义字符串的方式
php中定义字符串的方式

php中定义字符串的方式:单引号;双引号;heredoc语法等等。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

8874

2024.04.29

go语言字符串相关教程
go语言字符串相关教程

本专题整合了go语言字符串相关教程,阅读专题下面的文章了解更多详细内容。

4499

2025.07.29

c++字符串相关教程
c++字符串相关教程

本专题整合了c++字符串相关教程,阅读专题下面的文章了解更多详细内容。

4607

2025.08.07

Buffalo框架数据库开发全教程
Buffalo框架数据库开发全教程

本专题围绕Buffalo框架数据库开发,讲解database.yml多环境配置、soda与fizz迁移生成回滚、模型结构体标签、增删改查与条件查询、一对多与多对多关联、数据校验、回调钩子、事务处理及原生SQL执行能力。

60

2026.09.23

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn