繁殖力超強的兔子
說到斐波那契數,我們自然會想到曾經有一群繁殖力超強的兔子。比薩的商人斐波那契(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),所以這是一個不錯的選擇。
小結
斐波那契序列有很多迷人的性質和有趣的應用,本文僅為匆匆一瞥,希望能讓你對它多一些興趣。總之,繁殖力超群的兔子們在歡樂的繁殖着,而“兔子問題”也沒閑着,吸引着人們對它不斷地研究,並得到了廣泛應用。
參考