1. 程式人生 > 其它 >迴圈斐波那契數列_【演算法趣談】斐波那契數列

迴圈斐波那契數列_【演算法趣談】斐波那契數列

技術標籤:迴圈斐波那契數列

考慮一個數列,它的第一項和第二項都是1,從第三項開始,每一項都是前面兩項的和。

1,1,2,3,5,8,13,21....

這個數列,就是數學中著名的斐波那契數列了。它被義大利數學家列昂納多·斐波那契(Leonardoda Fibonacci)在1202年提出,也因此得名。今天的問題,就是求這個數列的第N項是多少。

(解決這個問題,本文會提供三種方法。後面兩種需要用到快速冪,可從本公眾號演算法趣談—快速冪中獲取詳解)

我們把這個數列的定義數學化一下,口語化的定義是:它的第一項和第二項都是1,從第三項開始,每一項都是前面兩項的和。。而將其數學化,我們令該數列的第i項為f[i],則把口語化表達翻譯過來就是:f[1]=1,f[2]=1,f[i]=f[i-1]+f[i-2](i>2)

像上面這樣的公式,我們稱其為遞推式。也就是通過前面能推出來後面的式子。凡是這種遞推式,都可以用迴圈來求出數列的第n項,程式碼應該比較好理解。

#include #include using namespace std;int n,f[100010];int main(){   cin>>n;  f[1]=1;  f[2]=1;  for(int i=3;i<=n;i++)    f[i]=f[i-1]+f[i-2];//遞推式  cout<  return 0;}

這種演算法,程式需要進行n次迴圈才可以算出來。他模擬的過程就相當於你一步一步推,f[2],f[3],f[4].....看起來十分費勁。那麼,有沒有更快的演算法?數學好的同學可能已經想出來了,我們是否可以把f[i]=f[i-1]+f[i-2]的通項公式給求出來,這樣就可以用一次計算求出來答案了。

首先,我們用待定係數法,設(其中α和β是兩個引數):

f0f8d83a1ca6cdf2900299566fd109dc.png

把αan-1挪過去,就可以得到:

e033df6b054abe027d194cbdc776f925.png

根據原本的遞推式,我們可以得知:

d7684682af02c34a752dee7707374f76.png

仔細觀察這個式子,如果α和β都是一個一元二次方程的根,那麼這個方程組豈不是像韋達定理?那我們構造一個一元二次方程,令α、β為這個方程的兩個根,且它們的關係符合上面的方程組。這樣的方程比較多,但無論如何它們的解都不會變。我們就構造一個簡單點的:

6a801c7a9b64eff2694951b5af7092fd.png

這個方程的解是:

e4b534630d4086631fb668629d1d325c.png

也就是說:(α β可顛倒順序)

4019538f420f50e439573b0a650254b0.png

我們再把α和β帶回原來的遞推式(注意因為α和β可以交換順序,所以有兩個遞推式):

e6da14ffb8f2b9f3d886775ea5975a2f.png

這樣,我們就把原本的遞推式轉化為了兩個等比數列,這樣,就可以先分別求出這兩個等比數列的通項公式。

76f2cab277ddb7465110f1a11f6a2ccb.png

最後,讓:

ee8c6d6f522b955fbc99a832021d0a3e.png

再化簡,就可以求出通項式了!:

69de35fbec77ccd7ddd0d05e76211295.png

好不容易求出來通項式,把它應用於計算機吧!

#include #include #include using namespace std;double n; double ksm(double a,int b){//快速冪  double ans=1;  while(b){    if(b%2==1)  ans*=a;    a*=a;    b=b/2;  }  return ans;}int main(){   cin>>n;  printf("%lf",(ksm((1+sqrt(5))/2,n)-ksm((1-sqrt(5))/2,n))/sqrt(5));  return 0;}

這裡面快速冪的時間複雜度是O(logn),所以整個程式的時間複雜度也是O(logn).

我們來試驗一下程式碼:

047f108d1117926fa21f40ab8c531d31.png

十分正常,再換個大一點的數:

7e2b1bf9fb5b334906aa8cc98271cf71.png

???為什麼好好的斐波那契數列,蹦出來一個小數?這是什麼情況?

回到程式仔細觀察,我們發現根號5在計算機中儲存的形式是小數,也就是計算機存的根號5根本不是根號5,只是近似於它的一個小數罷了。在n較小,比如等於10時,因為精度還夠,所以不會影響答案。但是!一旦n變大,計算機儲存的小數的精度就不夠了。因此,答案就會有所影響了!

那麼,現在怎麼辦,用這個方法,雖然優化了時間,卻消耗了精度啊。

所以,只能考慮別的做法了。觀察一個矩陣M,它長這個樣子:

3618b07735c11194eeffa11f25bb3142.png

如果把M平方,就會變成這個樣子:

4a722741c234ca60f275c10c8e630445.png

而M的立方,是這個樣子:

9daf589c2338b84a5593eb0e2b0f461c.png

仔細觀察,發現這個矩陣M的n次方後的矩陣中,M(1,1) 和M(2,1)分別對應著斐波那契數列的第n+1項和第n項。也就是,只用計算這個矩陣的n-1次方,它的第一行第一例的數就是斐波那契數列的第n項!

為什麼呢?因為在矩陣運算時:

745dfa96e719c6f1a293fff7cd4f777c.png

當前矩陣的左上角記錄的是前一個矩陣左上和右上之和,當前矩陣的右上角記錄的是上一個矩陣的左上角。這樣就相當於完成了斐波那契的遞推。

這樣,我們把問題從求斐波那契數列的第n項變成了求這個矩陣的n-1次方。我們知道矩陣滿足乘法交換律,因此,我們可以用矩陣快速冪來解決此問題。

首先,我們要先寫出來矩陣的乘法。也就是目標矩陣的(i,j)為第一個矩陣的第i行和第二個矩陣的第j列每兩個數乘積之和。

inlinenodematri_mult(nodeA,nodeB){//其中node是一個含矩陣的結構體    node C;    for(int i=1;i<=2;i++)        for(int j=1;j<=2;j++)            C.a[i][j]=0;for(inti=1;i<=2;i++)        for(int j=1;j<=2;j++)for(intk=1;k<=2;k++)C.a[i][j]=C.a[i][j]+A.a[i][k]*B.a[k][j];    return C;}

然後,寫出快速冪。在這個快速冪中,不同的是把原先的乘法變成了矩陣乘法函式。而且,原本記錄冪的sum初始值是1,現在是一個對角線為1的單位矩陣,因為任何矩陣乘以這個單位矩陣都不會變,就像乘以1一樣。

inline ksm(node A,int b){    node sum;    for(int i=1;i<=2;i++)//構造單位矩陣        for(int j=1;j<2;j++)            if(i==j)  sum.a[i][j]=1;            else sum.a[i][j]=0;    while(b){//矩陣快速冪if(b&1)sum=matri_mult(sum,A);//把乘號替換成了矩陣乘法函式        A=matri_mult(A,A);        b/=2;    }    return sum;}

最後輸出矩陣n-2次方第一列兩個數的和就ok了!

完整程式碼如下:

#include #include #include #include #include #define MAXN 1010using namespace std;int n,k;struct node{    int a[110][110];};inline node matri_mult(node A,node B){//矩陣乘法    node C;    for(int i=1;i<=2;i++)        for(int j=1;j<=2;j++)            C.a[i][j]=0;    for(int i=1;i<=2;i++)        for(int j=1;j<=2;j++)            for(int k=1;k<=2;k++)        C.a[i][j]+=A.a[i][k]*B.a[k][j];    return C;}inline node ksm(node A,int b){//矩陣快速冪    node sum;    for(int i=1;i<=2;i++)        for(int j=1;j<=2;j++)            if(i==j)  sum.a[i][j]=1;            else sum.a[i][j]=0;    while(b){        if(b&1)  sum=matri_mult(sum,A);        A=matri_mult(A,A);        b/=2;    }    return sum;}node in;int main(){    cin>>n;in.a[1][1]=1,in.a[1][2]=1,in.a[2][1]=1,in.a[2][2]=0;//初始化矩陣    in=ksm(in,n-2);if(n<=2)cout<<1;//注意n<=2時矩陣快速冪不適用,需要特判。    else  cout<1][    return 0;}

總結一下,一共有三種求斐波那契數列第n項的方式。第一種是用遞推式,時間複雜度O(n)。第二種是用通項式,時間複雜度O(logn),但是精度會不夠。第三種時矩陣快速冪,時間複雜度O(logn),精度足夠。