疯狂整数的统计 | 豆包MarsCode AI刷题

87 阅读8分钟

问题描述

小C发现了一类特殊的整数,他称之为“疯狂整数”。疯狂整数是指只包含数字 '1' 和 '2' 的整数。举个例子,数字 12 和 121 都是疯狂整数。

现在,给定一个整数 N,你的任务是计算出所有小于或等于 N 的非负疯狂整数的数量。

思路解析

  • 我们可以通过递归的方式构建所有可能的疯狂整数,然后筛选出小于等于给定 N 的部分进行计数。
  • 从最基本的疯狂整数‘1’和‘2’出发,对于已有的疯狂整数,在其末尾添加‘1’或者‘2’就可以生成新的更长的疯狂整数。例如,已知有‘1’,添加‘1’得到‘11’,添加‘2’得到‘12’;对于‘2’,添加‘1’得到‘21’,添加‘2’得到‘22’,以此类推不断生成新的疯狂整数。
  • 在生成的过程中,每生成一个新的疯狂整数,就与 N 进行比较,如果小于等于 N,则对其计数,这样就可以得到所有满足条件的疯狂整数的数量。

解题步骤

1.首先初始化了一个变量 count 为 0,这个变量将用于统计满足条件的疯狂整数的个数。

2.接着定义了一个内部的递归函数 generate_crazy_numbers,用于生成疯狂整数并进行条件判断与计数。

3.然后通过分别调用 generate_crazy_numbers(1) 和 generate_crazy_numbers(2) 来启动递归生成过程,因为最小的非负疯狂整数就是从‘1’和‘2’开始的。

4.最后返回统计好的 count 值,也就是小于等于 N 的疯狂整数的数量。

学习具体函数

generate_crazy_numbers 函数

作用: 这个递归函数负责具体生成疯狂整数,并判断生成的疯狂整数是否小于等于给定的 N,若满足条件则对计数器 count 进行相应的增加操作,以此来统计个数。

参数含义: 接收一个参数 cur_num,它表示当前正在构建的疯狂整数。在递归过程中,这个值会不断变化,从初始的‘1’或者‘2’开始,逐步扩展生成更长的疯狂整数。

内部逻辑:

1.首先通过 nonlocal count 声明,使得在这个内部函数中可以修改外部函数 solution 中定义的 count 变量。

2.然后进行条件判断,使用 if cur_num <= N: 来检查当前正在构建的疯狂整数是否小于等于给定的上限 N,如果满足这个条件,说明当前的这个疯狂整数是符合要求的,就将 count 的值加 1

  1. 接下来进行递归操作,目的是生成更多的疯狂整数:

(1)通过 generate_crazy_numbers(cur_num * 10 + 1),在当前的 cur_num 后面添加数字‘1’,形成一个新的更长的疯狂整数,然后继续以这个新整数作为参数递归调用自身,继续进行后续的判断和生成过程。例如,若当前 cur_num 是 1,那么经过这一步就会生成 11 并继续以 11 为参数递归下去。

(2)类似地,通过 generate_crazy_numbers(cur_num * 10 + 2) 在当前 cur_num 后面添加数字‘2’,同样进行递归调用,不断扩展生成新的疯狂整数。例如,若当前 cur_num 是 1,这一步就会生成 12 并接着以 12 为参数进行后续的递归处理。

优化思路

这种递归的实现方式在 N 值较大时可能会存在栈溢出的风险,因为递归的深度可能会变得很深。如果要处理较大的 N 值,可能需要考虑采用其他更优化的方法,比如数位动态规划等方式来避免递归过深的问题

一、关于递归实现方式存在栈溢出风险

1.递归的执行原理与栈的关系:

在程序中使用递归时,每当一个函数调用自身,系统会将当前函数的执行上下文(包括局部变量、当前执行到的代码位置等信息)保存到一个叫做 “调用栈” 的内存区域中,然后进入新的函数调用。只有当内层的递归调用执行完毕返回后,才会从栈中取出之前保存的执行上下文,继续执行外层函数剩下的部分。

2.递归深度过深导致栈溢出问题:

对于像之前提到的通过递归生成 “疯狂整数” 的代码来说,它不断地在当前整数后面添加‘1’或者‘2’并递归调用自身来生成更多更长的疯狂整数。如果给定的 N 值非常大,意味着需要生成的疯狂整数的长度会很长,也就会导致递归调用的次数极多。

随着递归调用次数的不断增加,调用栈会不断地压入新的执行上下文,占用越来越多的栈空间。而计算机系统中,栈的大小是有限制的(不同的编程语言、操作系统和运行环境下这个限制会有所不同,但总归是有限的),当递归调用的深度超过了栈所能容纳的极限时,就会引发 “栈溢出” 错误,程序也就无法正常运行下去了。
比如,若 N 是一个非常大的数,使得递归生成疯狂整数时需要递归成千上万层甚至更多,那大概率就会超出栈的容量,导致栈溢出情况发生。

优化方法

数位动态规划等更优化方法的优势及原理(以数位动态规划为例)

数位动态规划的基本思路:

数位动态规划是一种利用数字的数位特征来解决问题的动态规划方法。它把要处理的整数按照数位(比如个位、十位、百位等)进行拆分,然后从高位到低位依次去考虑每个数位上的取值情况,通过记录中间状态(一般使用数组等数据结构来存储状态信息)并利用状态转移方程来逐步推导出最终的结果,避免了像递归那样无限制地深入调用。

以处理 “疯狂整数” 问题为例说明数位动态规划的应用及避免栈溢出原理:

对于计算小于等于 N 的疯狂整数个数的问题,可以定义状态。比如定义 dp[i][0] 表示考虑到第 i 位(从高位开始,最低位为第 0 位),且当前组成的数小于 N 的前 i 位对应的疯狂整数的数量;dp[i][1] 表示考虑到第 i 位,且当前组成的数等于 N 的前 i 位对应的疯狂整数的数量。

然后根据 N 在每个数位上的数字情况以及上一位的状态来更新当前位的状态,也就是建立状态转移方程。例如,如果 N 的第 i 位数字是‘1’,那么 dp[i][0] 可以由 dp[i - 1][0] (前 i - 1 位小于 N 的前 i - 1 位时的疯狂整数数量)通过一定规则更新,因为当前位可以取‘1’且小于 N 的当前位,同时 dp[i][1] 也可以根据上一位状态以及当前位是否刚好等于 N 的这一位来更新。

这样,我们只需要按照数位依次去计算和更新这些状态,而不需要像递归那样层层深入地去生成每一个具体的疯狂整数,极大地减少了额外的空间占用(不需要庞大的调用栈来保存大量递归调用的执行上下文),也就有效避免了栈溢出的问题,并且能够高效地计算出满足条件的疯狂整数的数量,即便 N 的值很大也能较好地处理。

代码展示

屏幕截图 2024-11-26 094940.png

优化代码

    num_str = str(N)
    length = len(num_str)
    # dp[i][0]表示考虑到第i位且小于N的前i位对应的疯狂整数数量
    # dp[i][1]表示考虑到第i位且等于N的前i位对应的疯狂整数数量
    dp = [[0, 0] for _ in range(length + 1)]
    # 初始化边界条件,当没有数位时(即空数字),只有一种情况(即数字0,也算符合要求的一种情况),数量为1
    dp[0][0] = 1
    for i in range(1, length + 1):
        digit = int(num_str[i - 1])
        # 上一位小于的情况,当前位可以取1和2,都能保证整体小于,数量翻倍
        dp[i][0] += dp[i - 1][0] * 2
        # 上一位等于的情况,如果当前位取0,数量为0;取1,数量继承上一位等于的情况;取2,数量继承上一位等于的情况;取大于2,数量为0
        if digit >= 1:
            dp[i][0] += dp[i - 1][1]
        if digit >= 2:
            dp[i][1] += dp[i - 1][1]
        if digit == 1:
            dp[i][1] += dp[i - 1][0]
    return dp[length][0] + dp[length][1]

# 示例用法
N = 21
result = count_crazy_integers(N)
print(result)