1. 程式人生 > >斐波納契數列(Fibonacci Sequence)

斐波納契數列(Fibonacci Sequence)

斐波納契數列(Fibonacci Sequence 


0.前言

很久以前就想寫一些競賽學習的總結,但是由於之前事情比較多,導致計劃不斷的減緩。現在,大學教學任務的考試已經全部結束了,而比賽也告一段落,所以有時間來整理一下之前學過的東西。不久前,在做比賽的時候遇到了這樣一個問題:求出第N斐波納契數的前M位和後K位。所以就將斐波納契數列(Fibonacci Sequence作為第一步吧。(後面我簡稱斐波那契數列為Fib,函式Fib(x),代表第x個斐波那契數)

1.從兔子說起

問題:一般而言,兔子在出生兩個月後,就有繁殖能力,一對兔子每個月能生出一對小兔子來。如果所有兔都不死,那麼一年以後可以繁殖多少對兔子?

    解: 幼仔對數 = 前月成兔對數  

            成兔對數 = 前月成兔對數+前月幼仔對數  

            總體對數 = 本月成兔對數+本月幼仔對數 

            得到通向公式:an+2 = an+1 + ana1 = 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 ] = 1Fib[ 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 = 1K=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的最神奇的地方就是它與黃金分割的關係了:

這個可以用通向公式簡單的證明。在做二分查詢的時候可以利用黃金比例分為而不是對半分割,可能會效率更高。

自然界中對於黃金分割的詮釋無處不在,最後在這裡貼幾張圖片來感受一下他的神奇:

 

這兩張圖片是從下面的圖片中抽象出來的: