斐波那契數列的幾種計算機解法
斐波那契數列傳說起源於一對非常會生的兔子。定義:
這個數列有很多奇妙的性質(比如 F(n+1)/F(n) 的極限是黃金分割率),用計算機有效地求解這個問題的解是一個比較有意思的問題,本文一共提供了4種解法。
解法一:遞迴
這是最最最直觀的想法,是每個人都能編寫的簡單程式,優點是非常明顯的:簡單易懂,清晰明瞭。但是缺點就是效率非常低,時間複雜度是指數級的。舉個例子,比如要計算F(5),那麼就要就算F(4)+F(3),而在計算F(4)的時候又要計算F(3),導致了 F(3)的重複計算,如果n越來越大,重複的計算量是無比巨大的,這就是瓶頸所在。
程式碼:
int F(int n) { if(n <= 0) return 0; else if(n == 1) return 1; else return F(n-1) + F(n-2); }
那麼怎麼克服這個問題?這就引出瞭解法二。
解法二:動態規劃
解法一的缺點是因為重複計算,那麼我們只需要把一些已經計算過的答案存放起來,那這個缺點就解決了。我們用一維陣列來實現,比如 F(5)就存放在陣列下標為5的資料單元裡。
程式碼:
#include<iostream> using namespace std; int F(int n) { if(n<=0) return 0; if(n==1) return 1; int* ans = new int[n+1]; ans[0] = 0; ans[1] = 1; for(int i=2; i<=n; i++) ans[i] = ans[i-1] + ans[i-2]; int tmp = ans[n]; delete[] ans; return tmp; }
這個演算法的時間複雜度是O(n),空間複雜度也是O(n)。複雜度來到了線性,這是我們所高興的,但是,是否還有比線性更好的複雜度?
解法三:求解通項公式
如果我們知道了通項公式,那麼我們就能在 O(1)的時間內得到F(n)。這是一個完美的時間複雜度。
這裡只介紹一種求解通項公式的技巧——矩陣。矩陣作為一個強大的數學工具有太多不為人知的應用。當然還有其它方法,比如高中數學競賽裡面的特徵方程,有興趣的讀者可以自行搜尋一下。
我們很容易發現:
所以剩下的問題就是隻要求出了就求出了F(n)。
求這個矩陣的 n次方的解法也有很多,這裡介紹一種方法——相似對角化。
令
於是
上述方程的解為
於是解得
的基礎解係為
的基礎解係為
所以令
我們有:
所以,
兩邊取n次方,我們得到:
最後,做矩陣運算(實際上我們只需要 An 裡左下角的資料),便可以得到:
通項公式的計算就完成了。(推導過程需線性代數基礎)
時間複雜度是完美了,那麼有沒有缺點呢?當然有,公式裡引入了無理數,所以不能保證運算結果的精度。
解法四:分治
解法三的缺點是精度無法保證,那麼我們自然就想到,然計算機自己去計算,進行n-1次矩陣乘法不就行了。這是最直觀的想法,雖然是線性的,但複雜度還是不令人滿意,有沒有更好的複雜度?比如 log2 (n)?答案是有的。
先來看一個背景知識:一個十進位制正數 n的用二進位制表示要用floor( log2(n) )+1 位。(floor(x)返回不大於 x的最大整數)
用二進位制方式表示 n:
所以
如果能得到的值就可以經過 log2 (n)次乘法得到。
顯然可以通過遞推得到:
程式碼:
Class Matrix; //假設已經實現了矩陣類
Matrix MatrixPow(const Matrix &m, int n) //計算m的n次方
{
Matrix result = Matrix::identity; //單位矩陣
Matrix tmp = m;
for(; n; n >>= 1)
{
if(n & 1)
result *= tmp;
tmp *= tmp;
}
}
int F(int n)
{
Matrix an = MatrixPow(A, n);
return F1*an(1,0) + F0*an(1,1); //an(1,0)表示an的第1行第0列的元素
}
時間複雜度僅為O(log2 (n))。
參考資料: [3] 線性代數教材