兔子繁殖问题的递归及优化算法讨论
0. 背景引入
最近笔者在豆包 MarsCode AI 刷题题库里面刷题练习时,刷到一道兔子繁殖问题的算法题,刚好这学期笔者在学校的算法课也刚好有讨论与这相关的算法实现。
笔者认为这是一道非常适合新手入门的算法题,觉得特别值得拿出来和新手们(包括笔者自己)分享一下,而且学cs或者数学的同学应该都听说过这个问题,所以笔者选择了这道题来做一个题目解析和代码讲解。
兔子繁殖问题是题库里面的一道简单题,同时它也是一道非常经典的斐波拉契数列问题的应用题。
1. 题目复现
笔者先把题库里面的题干copy一次到这里方便大家阅读。
问题描述
问题描述
生物学家小 R 正在研究一种特殊的兔子品种的繁殖模式。这种兔子的繁殖遵循以下规律:
每对成年兔子每个月会生育一对新的小兔子(一雌一雄)。
新生的小兔子需要一个月成长,到第二个月才能开始繁殖。
兔子永远不会死亡。
小 R 从一对新生的小兔子开始观察。他想知道在第 A 个月末,总共会有多少对兔子。
请你帮助小 R 编写一个程序,计算在给定的月份 A 时,兔子群体的总对数。
注意:
初始时有 1 对新生小兔子。
第 1 个月末有 1 对兔子:原来那对变成了成年兔子,并开始繁殖。
第 2 个月末有 2 对兔子:原来那 1 对成年兔子,繁殖了 1 对新生的小兔子。
从第 3 个月开始,兔子群体会按照上述规律增长。
输入与输出
输入
一个整数 A(1 ≤ A ≤ 50),表示月份数。
返回
一个长整数,表示第 A 个月末兔子的总对数。
示例
输入:A = 1
返回:1
输入:A = 5
返回:8
输入:A = 15
返回:987
2. 题目解析
2.1.什么是斐波拉契数列?
斐波那契数列(Fibonacci sequence),又称黄金分割数列 ,因数学家莱昂纳多· 斐波那契 (Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称“兔子数列”,其数值为:1、1、2、3、5、8、13、21、34……在数学上,这一数列以如下 递推 的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N)*
2.2. 常规做法:递归算法
看到这里,结合上面斐波拉契数列的公式定义,大家也许会想到:诶,这道题用递归实现不就出来了吗?确实,笔者一开始也是这么实现的。 所以接下来,笔者将为大家展示一下比较常规的递归算法实现思路。(PS:笔者先会使用伪代码的形式去讲解算法设计思路。而下面还会有专门的完整代码展示环节。)
2.2.1.算法设计思路
既然斐波拉契数列是形如“前一项”+“前二项”的形式组成,我们不妨自定义一个函数,f(int A),并且传递月份参数A(和题干要求一样)。
别忘了,月份应该是一个int型的参数。
f(int A)的定义如下:
if (A==1){
return 1; //对应斐波拉契数列定义中 n=1的情况,应当返回1
}
if (A==2){
return 2; //对应斐波拉契数列定义中 n=2的情况,应当返回2
}
else {
return (f(n-1)+f(n-2)) //对应斐波拉契数列定义中 n>=3的情况,递归实现
}
以上的代码思路非常易懂,只需要使用if-else分支去根据月份变量在不同的区间,而引导返回不同的结果。 而当月份>=3时,函数将它自身作为返回值,也就是递归。变量n将因为递归而递减,直到n=1或者n=2的时候,满足其他if分支的条件,而结束递归。
2.2.2.算法实现
笔者将使用C语言和Python分别实现上述的递归算法。 先来看C语言实现算法的完整代码,并且提供题库里面的测试集进行测试:
#include <stdio.h>
solution(int A) {
if(A==1) {
return 1;
}
if(A==2) {
return 2;
}
else {
return (solution(A-1)+solution(A-2));
}
}
int main()
{
printf("%d\n", solution(1));//1
printf("%d\n", solution(2));//2
printf("%d\n", solution(5));//8
printf("%d\n", solution(15));//987
return 0;
}
下面来看一下使用Python实现的完整代码,并且提供题库里面的测试集进行测试:
def solution(A):
if A==1:
return 1
elif A==2:
return 2
else:
return (solution(A-1)+solution(A-2))
if __name__ == "__main__":
# Add your test cases here
print(solution(1) == 1) #True
print(solution(2) == 2) #True
print(solution(5) == 8) #True
print(solution(15) == 987) #True
3. 算法评价
当笔者按照这个思路来完成这道算法题的时候,我还顺便和MarsCode AI进行交流,发现这个算法其实并不是最优的算法,并且MarsCode为笔者推荐了一些优化算法实现。
我们先来看一下,递归算法的好坏。
它的好处不言而喻:
- 十分简单,有手就行。只要你学过if-else和递归的概念,基本上都可以实现。
- 简洁明了,递归算法通常能够用非常简洁的代码实现复杂的逻辑,使得问题的解决过程更加直观。
但是,它也有其弊端:
- 效率低下:当斐波拉契数列的变量非常大时,也就是当兔子繁殖持续的月份很大时,算法计算需要的时间和空间非常大。比如说,当A=15时,返回solution(15)的结果,竟然需要
- 栈溢出:递归算法在求解过程中会使用系统栈来保存每一层递归调用的状态。当递归深度过大时,可能会耗尽系统栈空间,导致栈溢出错误。这种错误在处理大规模问题时尤为常见,严重时甚至会导致程序崩溃。
4. 优化算法有哪些?
刚刚说过递归算法存在一定的弊端和缺陷,而Marscode AI为笔者推荐了以下两个优化算法的角度:
- 动态规划优化:使用数组存储计算过的斐波那契数
- 迭代方法优化:使用变量存储计算过的斐波那契数
下面笔者来分别解读一下这两种优化算法的代码设计思路。
4.1. 数组存储优化算法
在这个模式里面,我们需要定义一个数组fib,去分别存储已经计算好的斐波那契数。
4.1.1.代码思路概述:
-
step 0:首先我们先对数组fib初始化为 元素全为0的数组。
-
step 1: 当A=1或者2的时候,直接返回值为1或者2.
-
step 2:对数组前2个元素(也就是索引为0和1的元素),
fib[0]和fib[1]赋值为1.(因为这样子可以方便存储计算当A大于2之后的月份元素对应的值。) PS:这里有一个易错点,新手朋友要注意,我们人的常规思维会从1开始计数,但数组的索引值是从0开始算的。 -
step 3:当A>2后,返回
fib[n-1]+fib[n-2]的值。
4.1.2.完整代码实现:
同理,笔者也将会提供C语言和Python的算法实现代码给大家。
C语言实现:
#include <stdio.h>
solution(int A) {
if (A == 1) {
return 1;
}
if (A == 2) {
return 2;
}
int fib[A+1];
fib[0] = 1;
fib[1] = 1;
for (int i = 2; i <= A; i++) {
fib[i] = fib[i-1] + fib[i-2];
}
return fib[A];
}
int main() {
printf("%d\n", solution(1));//1
printf("%d\n", solution(2));//2
printf("%d\n", solution(5));//8
printf("%d\n", solution(15));//987
return 0;
}
Python实现:
def solution(A: int) -> int:
if A == 1:
return 1
elif A==2:
return 2
# 使用数组来存储斐波那契数列
fib = [0] * (A + 1)
# 修改了ai提供的参考答案
# fib[1] = 1
# fib[2] = 1
fib[0] = 1
fib[1] = 1
# 计算斐波那契数列
# for i in range(3, A + 1):
for i in range(2, A + 1):
fib[i] = fib[i - 1] + fib[i - 2]
return fib[A]
if __name__ == "__main__":
# Add your test cases here
print(solution(1) == 1) #True
print(solution(2) == 2) #True
print(solution(5) == 8) #True
print(solution(15) == 987) #True
4.2. 变量存储优化算法
在这个模式里,我们定义并赋值两个变量a, b = 1, 2去存储计算过斐波那契数。
为什么是定义两个变量呢?其实它们的作用主要是为了可以在每次迭代的时候,传递计算过的斐波那契数,防止数据丢失。(因为每一次迭代的时候,上一轮计算过的数据都会被新传进来的值覆盖掉)
4.2.1.代码思路概述:
- step 0:定义并初始化变量a和b。
- step 1: 当A=1或者2的时候,直接返回值为1或者2.
- step 2:当A>2后,使用变量
a去存储上一轮迭代中计算好的斐波那契数(兔子数)a = b.使用变量b去存储这一轮计算好的斐波那契数b = a + b. (PS:大一刚接触编程的同学们要记得分辨=和==的区别哟,=是用来传值的,==才是我们人认为的等于号)
4.2.2.完整代码实现:
同理,笔者也将会提供C语言和Python的算法实现代码给大家。
C语言实现:
#include <stdio.h>
solution(int A) {
if(A==1) {
return 1;
}
if(A==2) {
return 2;
}
int a=1,b=2,temp=0;
for (int i = 3; i <= A; i++) {
temp = a + b; //注意:在C语言里面,不能写成 a=b; b=a+b; 因为a+b的值会被覆盖掉,所以需要用一个临时变量temp来存储a+b的值。
a=b;
b=temp;
}
return b;
}
int main() {
printf("%d\n", solution(1));//1
printf("%d\n", solution(2));//2
printf("%d\n", solution(5));//8
printf("%d\n", solution(15));//987
return 0;
}
值得注意的是:在C语言里面,a和b的传值情况不能写成 a=b; b=a+b; 因为a+b的值会被覆盖掉,所以需要用一个临时变量temp来存储a+b的值。
Python实现:
def solution(A: int) -> int:
if A == 1:
return 1
elif A==2:
return 2
# 初始化前两个斐波那契数
# 修改ai提供的代码
# a, b = 1, 1
a, b = 1, 2
# 计算斐波那契数列
for _ in range(3, A + 1): #从A=3开始算,一直到算完A=给定数为止。
a, b = b, a + b
return b
if __name__ == "__main__":
# Add your test cases here
print(solution(1) == 1) #True
print(solution(2) == 2) #True
print(solution(5) == 8) #True
print(solution(15) == 987) #True
5. 总结
以上的两种优化算法都可以显著提高算法的效率,特别是在对于A很大的情况下的计算。
不过我个人会比较喜欢用数组存储的方式来计算,因为数组不会将每一轮的结果覆盖掉,这样子可以使得我们能够查询到在整一个给定月份值的兔子繁殖过程中,每一个月份对应的兔子数。
另外,我觉得,有时候虽然我们是可以靠自己来完成一个算法题的编写,但是我们自己想到的方法,不一定是最好的算法。所以在这个时候,借助一下AI的力量来优化自我是很有必要的。
(但是也不要太沉迷AI了,因为有时候AI提供的代码也不完全是正确的,毕竟在不同实际场景里总是需要人去动动脑子去变化一下代码的)