代码改变世界

迷人的斐波那契数

2012-04-19 00:31 by Anders Cui, ... 阅读, ... 评论, 收藏, 编辑

繁殖力超强的兔子

说到斐波那契数,我们自然会想到曾经有一群繁殖力超强的兔子。比萨的商人斐波那契(Fibonacci,12-13世纪,称为比萨的列奥那多)接触到阿拉伯数学后,在其著作《Liber Abaci》中,引入了这个著名的兔子问题。但如果向前追溯下去,则可以追溯到古老的印度数学。斐波那契使用了一个理想化了的兔子生长模型进行研究,并假设:

  • 第一个月初有一对刚诞生的兔子
  • 两个月之后(第三个月初)它们可以生育
  • 每月每对可生育的兔子会诞生下一对新兔子
  • 兔子永不死去

从第一个月开始,兔子的数目(对)依次是:1,1,2,3,5,8。。。这样就形成了一个序列,记为{Fn},则该序列存在一个递推关系:F(n)=F(n-1)+F(n-2),n >= 3。可以通过如下简单的推断得出:F(n)表示在第n个月时兔子的对数,这些兔子分为两部分,一是在第n-1个月已有的兔子,因为它们都活了下来(实际上会一直活下去),也就是F(n-1);二是F(n-1)对兔子中可生育的兔子,也就是已经生活了至少二个月的兔子,这个数字恰好是在第n-2个月已有的兔子,即F(n-2)。如果令F(0)=0,则F(n)的定义可推广至所有非负整数(公式一):

  • F(0)=0
  • F(1)=1
  • F(n) = F(n-1) + F(n-2),当n > 1

递归求值

这看起来是个非常简单的递推关系,若要通过程序求值, 很自然地,可通过递归实现(算法一):

// 递归算法 by Anders Cui
public
int Fib(int n) { if (n <= 1) { return n; } return Fib(n - 1) + Fib(n - 2); }

很多书籍在讲解递归求解时,也常常会选择斐波那契数作为例子。这个例子是一个很直观的递归应用,也很有趣,但却算不上一个好的应用。比如,求F(5)时可表示为下图:

即使对5这样小的数进行求值,也可以看到一点儿端倪,F(3)、F(2)都要多次求值,如果n的值很大,那么其中计算的冗余就非常多了。想办法来改进一下,最直接的办法就是要重用每一项之前的项,这样计算每一项时只需要一次加法。比如,要计算F(n),就创建一个长度为n+1的数组,逐一计算每一项,F(n)的时间复杂度和空间复杂度都是O(n),这是动态规划的应用。进一步地,按上面的方法,我们创建了一个长度为n+1的数组,但实际上每次运算只会涉及到三个相邻的元素,这就意味着只需要三个变量就可以维护所需要的值了,从而可以将空间复杂度降低到O(1)(算法二):

// 算法二 by Anders Cui
public
static int Fib(int n) { if (n <= 1) { return n; } int previousButOne = 0; int previous = 1; int answer = 1; for (int i = 2; i < n; i++) { previousButOne = previous; previous = answer; answer = previousButOne + previous; } return answer; }

那有没有更高效的算法?别忘了,本文的主题是斐波那契序列,在考虑序列时,往往需要分析它的通项公式,如果能找出它的通项公式,那么就可以快速得出答案了。

斐波那契序列的通项公式

前面提到F(n)的递推公式是F(n) = F(n-1) + F(n-2),即F(n) - F(n-1) - F(n-2) = 0,这是一个所谓“带常系数的齐次二阶线性递推式”,由其特征方程及初始值F(0)和F(1),可以求出通项公式为(过程略):

这个公式令人称奇之处在于,通过无理数的乘方表示出了一个整数序列的所有元素!通过此公式还可以确定F(n)是呈指数级增长的。不管怎样,根据通项公式可以在O(1)时间内得到答案(算是算法三吧),但计算机不能完全精确地表示无理数,从而无法保证计算的精度

不过一旦有了通项公式,研究兔子序列时就方便多了。下面将讨论算法一(递归算法)的效率。

在使用递归进行计算的时候,算法的基本操作无疑就是加法。用A(n)表示计算F(n)时所需要的加法次数,则在n>1时,有A(n) = A(n-1) + A(n-2) + 1,与F(n)递推式不同,这是一个非齐次递推式,其求解方法有所不同,不过对于这里的A(n),却可以快速解出。将上式变形为:

[A(n) + 1] - [A(n-1) + 1] - [A(n-2) + 1] = 0,

如果令B(n) = A(n) + 1,则B(n) - B(n-1) - B(n-2) = 0,B(0) = 1,B(1) = 1。到这里可以发现,B(n)与F(n)递推式相同,不过前者从1,1开始,后者从0,1开始,即B(n) = F(n+1)。所以A(n) = F(n+1) - 1,从而得出A(n)也为指数级。这是斐波那契序列的另一个奇妙之处:通过自身的通项公式了解自身的计算特点。

性质、变形与应用

斐波那契序列有为数众多的有趣性质,甚至有专门讨论它的杂志,这里不再赘述。

由本文开头的递推关系给出,其中两个要素是递推关系和初始值。如果对这两个要素进行调整,就可以得到其它相关的序列,如卢卡斯数、反斐波那契数等等。另外,斐波那契数不仅仅是一个数字谜题,它也有很多应用,这里仅给出两个例子。

爬梯子

假设每一步可以爬一格或者两格梯子,爬一部n格梯子一共可以用几种不同的方法?(比如三格的梯子有三种不同的爬法:1-1-1,1-2,2-1)

在分析这个问题的时候,也可以考虑用递归。假设爬n格梯子有A(n)种不同的方法,第一步要么爬一格,此时剩下的格子爬完共有A(n-1)种;要么爬两格,此时剩下的格子爬完共有A(n-2)种,从而得到递推关系A(n) = A(n-1) + A(n-2),初始值为A(1) = 1,A(2) = 2。可以看出A(n)与F(n)的关系是:A(n) = F(n + 1),n > 0。

F(n)在很多这样的组合题中都有应用,另外F(n)的一个组合证明可以看这里

欧几里德算法(求最大公约数)的效率分析

欧几里德算法据说是史上第一个算法,这两个古老的算法也有重要的交集。首先给出F(n)的一个性质(可通过数学归纳法证明):

然后通过辗转相除法的定义和F(n)的这个性质可以证明:欧几里德算法求解GCD(a, b)时使用的除法次数不大于b的十进制位数的5倍。再由对数的性质可以得出欧几里德算法使用O(logb)次除法就可以求出GCD(a, b)。

(涉及很多公式书写,这里证明从略,具体过程可参考《离散数学及其应用》的3.4节)

另一个算法

F(n-1)、F(n)和F(n+1)的关系也可以用矩阵表示出来:

这里n > 0,这样F(n)的计算可以转化为右边矩阵的乘方,而这个过程的时间复杂度是O(logn),所以这是一个不错的选择。


小结

斐波那契序列有很多迷人的性质和有趣的应用,本文仅为匆匆一瞥,希望能让你对它多一些兴趣。总之,繁殖力超群的兔子们在欢乐的繁殖着,而“兔子问题”也没闲着,吸引着人们对它不断地研究,并得到了广泛应用。

 

参考

《算法设计与分析基础》
《编程之美》
斐波那契数
Fibonacci Number