每日一水
问题描述
生物学家小 R 正在研究一种特殊的兔子品种的繁殖模式。这种兔子的繁殖遵循以下规律:
- 每对成年兔子每个月会生育一对新的小兔子(一雌一雄)。
- 新生的小兔子需要一个月成长,到第二个月才能开始繁殖。
- 兔子永远不会死亡。
小 R 从一对新生的小兔子开始观察。他想知道在第 A 个月末,总共会有多少对兔子。
请你帮助小 R 编写一个程序,计算在给定的月份 A 时,兔子群体的总对数。
注意
- 初始时有 1 对新生小兔子。
- 第 1 个月末有 1 对兔子:原来那对变成了成年兔子,并开始繁殖。
- 第 2 个月末有 2 对兔子:原来那 1 对成年兔子,繁殖了 1 对新生的小兔子。
- 从第 3 个月开始,兔子群体会按照上述规律增长。
解答
刚刚入门的同学在学习完递归去做练习一定看到过类似问题, 这题用递归方法很容易解出来. 实现思路基本在"注意"栏已经很明晰了, 初始为1对兔子,第二个月末为2对兔子(初始幼兔长大又生了一对), 后面以此类推. 不难推出第n月的兔子数量是前面两个月的兔子数量之和.
- 不太理解的同学可以参考以下草图:(Y为成兔,y为幼兔)
- 第一月: y
- 第二月: Y y
- 第三月: Y Y y
- 第四月: Y Y Y y y
- 第五月: Y Y Y Y Y y y y
- 第六月: Y Y Y Y Y Y Y Y y y y y y
- ...
- 这样就更直观明了了, n-1月的兔子都还活着, 只有n-2月的兔子才能繁殖幼兔. 代码如下
def solution(A: int) -> int:
# Edit your code here
if A == 1:
return 1
if A == 2:
return 2
return solution(A - 1) + solution(A - 2)
很显然, 一定会超时. 以5为例,
f(5)
f(4)+f(3)
[f(2)+f(3)] + [f(1)+f(2)]
[f(2)+f(1)+f(2)] + [f(1)+f(2)]
同学应该会发现,里面有很多重复的计算, 使得程序超时, 那么解决方法也很简单,就是将计算结果存储起来.# 记忆数组
memo = {}
def solution(A: int) -> int:
if A == 1:
return 1
if A == 2:
return 1
# 检查是否已经计算过
if A in memo:
return memo[A]
# 递归调用并存储结果
memo[A] = solution(A - 1) + solution(A - 2)
return memo[A]
- 记忆化递归可以显著提高递归方法的性能
还有一种更高效的解决方法是迭代法
也就是使用循环来计算每个月兔子对数,代码如下:
def solution(A: int) -> int:
# 初始化前两个月的兔子对数
if A == 1:
return 1
if A == 2:
return 1
# 初始化前两个月的兔子对数
first, second = 1, 1
# 从第3个月开始计算
for _ in range(3, A + 1):
# 计算当前月的兔子对数
current = first + second
# 更新前两个月的兔子对数
first, second = second, current
return second
使用迭代法的好处是:
时间复杂度
迭代方法:
- 迭代方法通过一个循环从第3个月开始计算到第
A个月 - 每次循环中,我们只需要进行一次加法操作(
current = first + second) - 因此,循环总共执行
A - 2次(从第3个月到第A个月) - 时间复杂度为
O(A)
递归方法:
- 递归方法通过递归调用
solution(A - 1)和solution(A - 2)来计算结果。 - 每次递归调用都会产生两个新的递归调用,直到递归终止条件(
A == 1或A == 2)。 - 递归树的深度为
A,每个节点最多有两个子节点。 - 因此,递归方法的时间复杂度为
O(2^A),这是一个指数级的时间复杂度。
空间复杂度
迭代方法:
- 迭代方法只需要存储前两个月的兔子对数(
first和second)。 - 无论
A多大,我们只需要固定数量的额外空间来存储这些变量。 - 空间复杂度为
O(1)。
递归方法:
- 递归方法在每次递归调用时都会在栈上分配新的空间
- 递归树的深度为
A,因此栈的最大深度也为A - 空间复杂度为
O(A)
迭代方法在时间和空间上都比递归方法更高效,特别是对于较大的 A 值。因此,在实际应用中,迭代方法通常是更好的选择。
至此问题解决