提要
本文介绍了4种(3种?)求斐波那契数列第n项的方法。
斐波那契数列简介
斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列以如下被以递归的方法定义: F(0)=1,F(1)=1,F(n)=F(n−1)+F(n−2)(n>=2,n∈N∗)
解法:
0.按照递推式直接计算,时间复杂度 O(n)
1.矩阵快速幂
矩阵乘法和矩阵快速幂的简介请看我的另一篇博客:http://blog.csdn.net/George__Yu/article/details/77231013
现在我们有这样一个矩阵A: (F[i−1]F[i]) 。
然后我们构造一个矩阵
见证奇迹的时刻到了!
我们发现: AB=(F[i]F[i+1]) (大家可以自己手算一下)
若i刚开始时等于1,即A= (01) ,那么 A 乘上 n 次 B 后, A12 就是斐波那契数列的第 n 项。
又因为矩阵乘法具有结合律,所以我们可以把 Bn 先用矩阵快速幂算出来,再乘上 A 即可。
这样,我们把递推斐波那契数列第 n 项的时间复杂度从 O(n) 降到了 O(log2n) 。
很神奇吧?
求斐波那契数列第n项的代码:
//矩阵快速幂求斐波那契数列第n项
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <cmath>
using namespace std;
typedef long long LL;
struct matrix{
LL z[5][5];
int m,n;
}origin,res,fib;
LL n;
LL p=1e9+7;
void print(matrix x)
{
for(int i=1;i<=x.m;i++)
{
for(int j=1;j<=x.n;j++)
printf("%d ",x.z[i][j]);
printf("\n");
}
printf("\n");
}
matrix mul(matrix x,matrix y)//(AB)ij=∑Aik*Bkj=Ai1*B1j+Ai2*B2j+...+Aik*Bkj
{
matrix t;
t.m=x.m;
t.n=y.n;
memset(t.z,0,sizeof(t.z));
for(int a=1;a<=x.m;a++)
for(int b=1;b<=x.n;b++)
for(int c=1;c<=y.n;c++)
t.z[a][c]+=x.z[a][b]*y.z[b][c];
for(int i=1;i<=x.m;i++)
for(int j=1;j<=x.n;j++)
t.z[i][j]%=p;
return t;
}
void matrixpow(LL n)
{
while(n)
{
if(n&1) res=mul(res,origin);
origin=mul(origin,origin);
n>>=1;
}
}
void init()
{
scanf("%lld",&n);
origin.z[2][1]=origin.z[2][2]=origin.z[1][2]=1;
//构造斐波那契数列的递推矩阵 | 0 1 |
origin.m=origin.n=2;// | 1 1 |
for(int i=1;i<=2;i++) res.z[i][i]=1;//构造单位矩阵 | 1 0 |
res.m=res.n=2; // | 0 1 |
fib.z[1][2]=1;//构造斐波那契数列第0项和第一项的矩阵| 0 1 |
fib.m=1;fib.n=2;
}
int main()
{
init();
matrixpow(n);
fib=mul(fib,res);
printf("%lld\n",fib.z[1][1]%p);
return 0;
}
推广:
然而问题还没结束。
矩阵B是怎么构造出来的?
若要求递推的数列是 F[i]=aF[i−1]+bF[i−2] 怎么办?
这时怎么构造矩阵B?
其实很简单。
这时矩阵 A=(F[i−1]F[i])
我们不妨设矩阵 B=(xzyw)
因为我们希望 AB=(F[i]F[i+1])=(F[i]aF[i]+bF[i−1])
根据矩阵乘法的定义又有 AB=(xF[i−1]+zF[i]yF[i−1]+wF[i])
那么容易知道 x=0,y=b,z=1,w=a
所以 B=(01ba)
所以 F[n]=(A×Bn)12
大家可以直接记住这个结论,如果记不住的话,在a,b已知时可以按刚才的方法解方程求x,y,z,w。
再推广:
如果递推式不止两项怎么办?比如这样:
F[i]=a1F[i−1]+a2F[i−2]+⋯+akF[i−k] 且 F[1],F[2],F[3],⋯,F[k]=1
还是按刚才的办法求B矩阵!
这里我直接给出结果,有兴趣的朋友可以自己研究一下。
B矩阵是 k 行 k 列的矩阵。
这时用矩阵快速幂递推的时间复杂度是 O(k2log2n) ,而直接递推的复杂度是 O(nk) 。
一般 n 都是远大于 k 的,所以绝大多数情况还是矩阵快速幂快。
(PS:我应该没算错,如果大家发现我算错了可以在评论中指出来qwq)
2.利用斐波那契数列的二倍项公式
二倍项公式:
通过这个公式,可以分治地求出斐波那契数列的第n项。
若n是偶数,我们想要知道F[n],只需要知道F[n/2]和F[n/2-1];
若n是奇数,我们想要知道F[n],只需要知道F[n/2+1]和F[n/2](除法默认向下取整)。
在 n 极大的时候(如 1018 左右),因为空间不够,我们用map而不是数组来储存中间结果。
由于我们每次都把数据折半,因此空间复杂度并不高,map所用的空间和递归所用的栈空间都是 log2n 级别的。
然而在时间复杂度上,因为用了map和递归计算,所以要比矩阵快速幂慢一些。大概是 O(log2n) 。
无论从空间上还是时间上,这种方法都要劣于矩阵快速幂。如果不会矩阵快速幂,可以先用这种方法。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <map>
#define mod 1000000007
using namespace std;
typedef long long LL;
LL n;
map<LL,LL>m;
map<LL,LL>::iterator it;//定义一个迭代器
LL F(LL i)//递归求解
{
LL res1,res2,res;
if(i<3) return 1; //F[1]=F[2]=1
it=m.find(i);
if(it==m.end())//未算过
{
if(i&1)//奇数使用公式(2)
{
res1=F(i>>1);
res2=F((i+1)>>1);
res=(res1*res1+res2*res2)%mod;
}
else//偶数使用公式(1)
{
res1=F((i-2)>>1);
res2=F(i>>1);
res=((res1<<1)+res2)*res2%mod;
}
m[i]=res;//记录
return res;
}
else
return it->second; //算过直接返回
}
int main()
{
scanf("%lld", &n);
printf("%lld\n", F(n));
return 0;
}
3.分段打表
如果数据范围是 n <= x,那么我们可以先把 F[x√],F[2x√],F[3x√],⋯,F[x] 都算出来,从这些项开始递推。这种方法的时间复杂度和空间复杂度都是 O(n√) ,而且要求 n 不能太大,在考试时间范围内能用暴力方法把 F[x] 先算出来。
代码就不贴了,这种方法比较玄学,大家也不一定非要以 x√ 为长度来打表,只要保证每一段的长度能在限定时间内算出来就可以(一般是 107 左右),自己意会一下就好(雾。
注意:这种方法只在n稍大于1e8的时候比较有用,比如1e10左右。n太大时还是乖乖用上面两种方法吧。
例题
洛谷【p1962】斐波那契数列
https://www.luogu.org/problem/show?pid=1962
题目大意:求斐波那契数列的第n项,n<= 1018 。(并不能分段打表233)
大家可以当模板题交一下自己的代码。