斐波纳契数列(Fibonacci Sequence)
0.前言
很久以前就想写一些竞赛学习的总结,但是由于之前事情比较多,导致计划不断的减缓。现在,大学教学任务的考试已经全部结束了,而比赛也告一段落,所以有时间来整理一下之前学过的东西。不久前,在做比赛的时候遇到了这样一个问题:求出第N个斐波纳契数的前M位和后K位。所以就将斐波纳契数列(Fibonacci Sequence)作为第一步吧。(后面我简称斐波那契数列为Fib,函数Fib(x),代表第x个斐波那契数)
1.从兔子说起
问题:一般而言,兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生出一对小兔子来。如果所有兔都不死,那么一年以后可以繁殖多少对兔子?
解: 幼仔对数 = 前月成兔对数
成兔对数 = 前月成兔对数+前月幼仔对数
总体对数 = 本月成兔对数+本月幼仔对数
得到通向公式:an+2 = an+1 + an,a1 = a2 = 1
这就是Fib的由来和递推公式的计算。
2.Fib的计算1:递推公式
利用递推公式:Fib(n+2) = Fib(n+1) + Fib(n),Fib(1) = Fib(2) = 1
可以直接想到最简单的计算方法,递归运算。
代码:
int Fib( n )
{
if ( n == 1 || n == 2 ) return 1;
else return Fib(n-1) + Fib(n-2);
}
分析:递归运算的Fib的时间复杂性为O(Fib(N))。
证明:设TFib(n) 为计算Fib(n)的运算次数,记TFib(1) = TFib(2) =O(1)
当n = 1时有,TFib(1) = O( Fib(1) ) 结论成立。
假设当n=k时结论成立,那么有TFib(k) = O( Fib(k) ),TFib(k-1)=O( Fib(k-1) )
当n=k+1时有,TFib(k+1) = O( Fib(k) )+ O( Fib(k-1) ) + O(1) = O( Fib(k+1) ),成立。
综上所述,结论成立。
说明:看完后面的通向公式,我们可以知道,这是一个指数级的算法。
3.Fib的计算2:动态规划
利用动态规划的思想:Fib[ 1 ] = Fib[ 2 ] = 1,Fib[ n ] = Fib[ n-2 ] + Fib[ n-1 ]
这个式子看起来和上面的很相似,但是却又很大的区别。我们发现在计算Fib(n-1)时Fib(n-2)又被计算了一次,所以导致大量的重复计算,为了避免重复计算,我们可以将之前计算出来的结果储存起来,这就是动态规划的思想了。
代码:
int Fib( int n )
{
int F[ MAXSIZE ];
F[ 1 ] = F[ 2 ] = 1;
for ( int i = 3 ; i <= n ; ++ i )
F[ i ] = F[ i-2 ] + F[ i-1 ];
return F[ n ];
}
分析:动态规划算法的时间复杂性为O(n)
说明:这个可以很简单的看出来,因为每个Fib只求解了一次。这是一个线性的算法。
4.Fib的计算3:分治法
a.快速幂:
这里先要说明一下快速幂的算法,即计算x^n的对数级算法。
其实这个很像前面的动态规划的思想,计算x^n,可以用x^(n/2) * x^(n/2) * K来计算,其中当n%2 = 1时K=x否则K=1。所以这个算法的运行就和二分查找相似了。
代码:
int qpow( int x, int n )
{
if ( n == 1 ) return x;
int v = qpow( x, n/2 );
if ( n%2 ) return v*v*x;
else return v*v;
}
分析:快速幂的时间复杂性为O(logN),这是一个对数阶算法。
说明:由于x^(n/2)只需要被计算一次就行,所以计算的过程就是,n -> n/2 -> n/4 -> ... ->1所以计算logN次。
这里也可以用递归式证明,T(n) = T(n/2) + O(1) => T(n) = O(logN)
b.Fib的矩阵表示:
设Jn为第n个月有生育能力的兔子数量,An为这一月份的兔子数量。得到如下递推矩阵。
其中
这个可以用数学归纳法简单的证明,这里就不做证明。
然后我们把上面的快速幂算法应用到矩阵中,就得到了一个对数级的Fib算法。
代码:
/* 矩阵快速幂,其中结果在Bas中 */
#define SIZE 2
#define MOD 10000007
long long Mat[ SIZE ][ SIZE ];
long long Bas[ SIZE ][ SIZE ];
long long Add[ SIZE ][ SIZE ];
/* 清零函数 */
void CLEAR( long long A[][ SIZE ], int m )
{
for ( int i = 1 ; i <= m ; ++ i )
for ( int j = 1 ; j <= m ; ++ j )
A[ i ][ j ] = 0LL;
}
/* 矩阵乘法 */
void MUL( long long A[][ SIZE ], long long B[][ SIZE ], long long C[][ SIZE ], int m )
{
CLEAR( A, m );
for ( int i = 1 ; i <= m ; ++ i )
for ( int j = 1 ; j <= m ; ++ j )
for ( int k = 1 ; k <= m ; ++ k )
A[ i ][ j ] = (A[ i ][ j ]+B[ i ][ k ]*C[ k ][ j ])%MOD;
}
/* 矩阵复制 */
void COPY( long long A[][ SIZE ], long long B[][ SIZE ], int m )
{
for ( int i = 1 ; i <= m ; ++ i )
for ( int j = 1 ; j <= m ; ++ j )
A[ i ][ j ] = B[ i ][ j ];
}
/* 矩阵快速幂 */
void POW( long long n, int m )
{
if ( n == 1LL )
COPY( Bas, Mat, m );
else {
POW( n/2LL, m );
COPY( Add, Bas, m );
MUL( Bas, Add, Add, m );
if ( n%2LL ) {
COPY( Add, Bas, m );
MUL( Bas, Add, Mat, m );
}
}
}
说明:这里可以利用取模运算计算出Fib(n)的后k位。
5.Fib的计算4:通向公式
如果我们知道一个数列的通向,那么求解这个数列就会容易很多。
计算Fib的通向的方法有很多,这里直接给出结论:
其正确性可以通过数学归纳法证明。
这里我们又回到了快速幂的计算上来了。
代码:
double qpow( int x, int n )
{
if ( n == 1 ) return x;
double v = qpow( x, n/2 );
if ( n%2 ) return v*v*x;
else return v*v;
}
分析:时间复杂性为O(logN)
说明:这里可以利用这个方法除以10^m 计算出Fib的前k位。
6.FIb恒等式
这里给出一些与Fib有关的恒等式,有可能会用到,但不做证明。(证明可使用数学归纳法)
A.F1 + F2 + F3 + ... + Fn = Fn+2 − 1
B.F1 + 2F2 + 3F3 + ... + nFn = nFn+2 − Fn+3 + 2
C.F1 + F3 + F5 + ... + F2n−1 = F2n
D.F2 + F4 + F6 + ... + F2n = F2n+1 − 1
E.(F1)^2 + (F2)^2 + (F3)^2 + ... + (Fn)^2 = FnFn+1
F.Fn-1Fn+1 - (Fn)^2 = (-1)^n
7.神奇的Fib
计算告一段落了,最后做一下Fib的宣传工作。
说道Fib的最神奇的地方就是它与黄金分割的关系了:
这个可以用通向公式简单的证明。在做二分查找的时候可以利用黄金比例分为而不是对半分割,可能会效率更高。
自然界中对于黄金分割的诠释无处不在,最后在这里贴几张图片来感受一下他的神奇:
这两张图片是从下面的图片中抽象出来的: