Python趣味算法入门 - 兔子产子(斐波那契数列的内在逻辑)

问题描述

有一对兔子,从出生后的第3个月起每个月都生一对兔子。小兔子长到第3个月后每个月又生一对兔子,假设所有的兔子都不死,问30个月内每个月的兔子总对数为多少?

分析

原书直接告诉读者本题是斐波那契数列,然后就将重点放在了如何用Python打印出斐波那契数列上面,这就好比直接把答案抛出,却未做更多解释——虽然这个问题本身就是斐波那契的典型例子,但关于原理却鲜有提及,这也让问哥产生了为本书补充代码的想法。

解法一

首先说一下如果我们并不知道结果是斐波那契数列,而是使用强大的计算机,所采取的比较直观的暴力穷举解法——递归:

每个月兔子生下的子兔子,还会在两个月后再生下孙兔子,子子孙孙的数量要加在一起。符合递归的特征:

  1. 有终止条件——30个月
  2. 不断重复相似的计算——每对新出生的兔子两个月后(第三个月开始)又会繁殖新兔子
  3. 初始条件不同——从第三个月开始,每个月都有新出生的兔子,相对这些新出生的兔子而言,它们的初始条件是不一样的

由于最终我们要返回的不是某个月存活的兔子总量(那样更简单一点),而是要列出30个月里每个月的兔子数量,换句话说,是一个包含30个数字的列表。于是这时我们可以决定要定义的递归函数的返回值是一个列表。

可以这样认为,递归函数代表了一对兔子,它们自出生那个月开始,每个月都存在,也就是说,如果是第一对兔子(祖兔子),不考虑后代的话,它们返回的应该是全为1的列表——代表30个月,每个月都有1对兔子。——此为函数的第一步。

但是由于从第三个月开始,这对兔子会繁殖一对新兔子(子兔子),就代表产生了一次递归调用。而这对子兔子(递归函数)返回的也是列表,只不过列表的长度是28——因为它们是从第3个月开始计算到第30个月,类似地,第四个月该对兔子又会繁殖一对新兔子,但返回的列表长度只有27,以此类推。——此为函数的第二步。

最后假设,这第一对子兔子也没有繁殖,那它们返回的应该是全为1的、长度为28的列表,但是我们最终是要得到的是每个月的总数,所以我们还是要把这个返回的列表里的数量加到祖兔子的列表里,所以我们需要遍历子兔子的列表,从祖兔子的列表第3个元素开始,依次累加。而第二对子兔子返回的长度为27的列表也要累加进去,以此类推。——此为函数的第三步,也是最重要的一步。

然而,事实是,在得到结果之前,子兔子还会繁殖孙兔子,于是在返回列表之前,还需要在第二步的时候继续向下递归,以期得到孙兔子的列表,同样的,长度递减。

于是就这样子子孙孙调用下去,直到第30个月,遍历结束,返回的列表长度为0,然后递归返回,一层一层向上累加,直到祖兔子。

代码实现如下:

def rabbit(month:int)->list:
    r = [1]*month # 这对兔子在每个月都要被计算在内
    for m in range(2,month): # 从第3个月开始,要加上子兔子,以及这些兔子在未来繁殖的兔子
        arr = rabbit(month-m) # 接收递归返回,列表长度递减
        for i in range(len(arr)):
            r[m+i]+=arr[i] # 加上子兔子及孙兔子及所有后代在每个月的数量
    return r

print(rabbit(30))

输出结果

[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040]

我打赌你们在别的地方从未见过为此题而写出的这么简单粗暴的代码,它的时间复杂度是阶乘阶(O(n!)),在实际计算中,一旦月份超过30,算出结果的时间将无法忍受。这就不得不让我们思考怎样去优化。

答案确实是斐波那契数列(本身也是斐老自己举的例子),于是我们可以使用斐波那契数列的计算方式。但是为什么会是斐波那契数列呢?它的内在逻辑是什么?

解法二

此题的历史比计算机诞生还要久,所以在没有计算机的时候,如果需要验证,我们要手工计算,把每个月的兔子数量列出来,所以也不可能采取上述的递归做法。

以下图为例,每一个图标代表一对兔子,用不同颜色表示第几代兔子,绿色表示祖兔子,蓝色表示子兔子,其他颜色表示不同父的孙兔子。可以看出,从第5个月开始,子兔子开始繁殖孙兔子,而从第7个月开始,孙兔子又将繁殖子兔子(画不下了),这样下去,无法手工统计。

但是如果用逆向思维来看,我们想知道第n个月的兔子数量,那么它和之前的兔子数量有什么关系呢?

首先,第n-2个月的兔子从第三个月开始,也就是第n个月开始,不管老的小的,都会繁殖新兔子,比如在n-2月有三对兔子,那么在第n个月肯定又有三对新出生的兔子,数量和第n-2月的兔子数量相等

那其他的兔子呢?我们来看第n-1个月的兔子。第n-1个月时无论是子又生孙、孙又生子,繁殖了多少对新兔子,总的兔子数量从属性上来看,只能分为两类

  1. 第n-2个月的兔子
  2. 第n-1个月新出生的兔子

而我们虽然不知道第n-1个月有多少对新的兔子出生,但可以肯定的是,这些新的兔子在第n个月不会繁殖新兔子(因为只有两个月),而它们的父母,虽然在第n个月会继续繁殖,但繁殖的兔子数量已经在上一步统计过了——与第n-2个月的兔子数量相等。所以,总体上来说,第n-1个月的兔子,在第n个月不会再繁殖新的兔子(除了已经统计过的n-2个月的兔子数量)。于是,我们可以直接把n-1个月的兔子的数量也统计到第n个月中来,与第n-2个月的兔子数量加在一起,便可以得到第n个月的兔子数量。

这便是为什么兔子产子问题是斐波那契数列背后的逻辑。

关于如何编写代码得到斐波那契数列,已经有太多例子了,这里不过多探讨,简要把代码列出。

a = b = 1
fib = [a]
while len(fib)<30:
    fib.append(b)
    a,b = b,a+b
print(fib)

我们可以扩展一下,如果问题的条件是每对兔子在第四个月,或者第五个月才可以繁殖新兔子,那应该怎样得到结果呢?

很显然,答案不再是传统的斐波那契数列,但如果掌握了上述的逻辑,就不难推断出:第n个月兔子的数量将等于第n-i-1个月的兔子与n-1个月的兔子数量之和,i为开始繁殖的月份。因为我们不必关心中间月份的兔子数量——反正它们繁殖的兔子总量将累加到第n-1个月中,而在中间月份新出生的兔子也不会繁殖新的兔子,除了第n-i-1月的那些兔子以外。

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

请叫我问哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值