關於菲波那契數列的一個低級問題?

問題是 f1=f2=1 輸入n 求fn除以10007的餘數 我用一個遞歸求fn 我確信都沒問題 用比較小的數據測試也ok 但問題是約定n數據小於等於1000000 我拿1000000測試就直接跪 該怎麼寫呀


表示這道題一點都不簡單,反而是一道技巧性非常強的題目(我非常喜歡這道題)。所以那種O(n)的方法都是不優秀的。下面給出一種O(log n)的演算法。等下我會給段程序,我稍微改了一下,用那個程序算了100000次1000000(沒有輸出),只用了0.107s。不會有哪個O(n)的演算法可以跑到這種程度的。這就是演算法的魅力。

主要思路是快速冪矩陣乘法的結合律

首先,要把這個問題化為一個矩陣運算的問題。

考慮下面的等式:

    egin{equation*}
    egin{split}
        egin{bmatrix}
            F_{n-1}  F_{n-2}\
            F_{n-2}  F_{n-3}
        end{bmatrix}
        cdot
        egin{bmatrix}
            1  1\
            1  0
        end{bmatrix}
        =
        egin{bmatrix}
            F_{n-1}+F_{n-2}  F_{n-2}+F_{n-3}\
            F_{n-2}+F_{n-3}  F_{n-2}
        end{bmatrix}\
        =
        egin{bmatrix}
            F_{n}  F_{n-1}\
            F_{n-1}  F_{n-2}
        end{bmatrix}\
    end{split}
    end{equation*}

        egin{bmatrix}
            F_{n+1}  F_{n}\
            F_{n}  F_{n-1}
        end{bmatrix}=
        egin{bmatrix}
            1  1\
            1  0
        end{bmatrix}^{n}

這樣,求F_n就變成了求egin{bmatrix}
            1  1\
            1  0
        end{bmatrix}^{n},而求一個量的次方就可以用快速冪的方法。這個方法你自己在網上搜一下還是很多,隨便貼個網址:快速冪_互動百科。

上代碼

#include &

using namespace std;

const int m=10007;

class matrix
{
public:
matrix();
long data[2][2];
matrix operator *(const matrix rig);
matrix operator =(const matrix rig);
};

matrix::matrix()
{
data[0][0]=1;
data[0][1]=1;
data[1][0]=1;
data[1][1]=0;
}

matrix matrix::operator *(const matrix rig)
{
matrix ans;
ans.data[0][0]=(data[0][0]*rig.data[0][0]+data[0][1]*rig.data[1][0])%m;
ans.data[0][1]=(data[0][0]*rig.data[0][1]+data[0][1]*rig.data[1][1])%m;
ans.data[1][0]=(data[1][0]*rig.data[0][0]+data[1][1]*rig.data[1][0])%m;
ans.data[1][1]=(data[1][0]*rig.data[0][1]+data[1][1]*rig.data[1][1])%m;
return ans;
}

matrix matrix::operator =(const matrix rig)
{
for(int i=0;i&<=1;i++) { for(int j=0;j&<=1;j++) { data[i][j]=rig.data[i][j]; } } return *this; } int main() { long n; cin&>&>n;
matrix k,ans;
while (n&>0)
{
if (n%2==1)
{
ans=ans*k;
}
k=k*k;
n=n/2;
}
cout&<&


遞歸易爆棧,一般幾萬層遞歸就爆了,何況上百萬的數據。

解決方法至少有以下的四種:

(對於題主所提到的1000000的數據量的情況,使用前兩種方法足矣,而對於數據量更加大的情況,請參考第三、四種方法)

一、如用遞歸寫法,則可以使用手動增棧命令

若用C++語言編寫該代碼且用VC++編譯器編譯,可用以下pragma命令增棧

#pragma comment(linker,"/STACK:1024000000,1024000000")

若用GCC/G++編譯,則可使用彙編命令來增棧(此處略)

二、可改成非遞歸寫法

#include &

const int mod = 10007;

int main() {
int n;
scanf("%d", n);
if(n &< 3) { puts("1"); } else { int a = 1, b = 1, c; for(int i = 2; i &< n; ++i) { c = (a + b) % mod; a = b; b = c; } printf("%d ", c); } return 0; }

三、尋找循環節

既然答案是只需取除以10007之後的餘數,設i&則一定存在f(k+1) % 10007 等於 f(i+1)%10007 且 f(k) % 10007 等於 f(i) % 10007,此時f(k+2) % 10007的值必定等於f(i+2) % 10007的值。

此時,我們只需要根據 f(j) ( i &<= j &< k ) 的值則可以推出 f(n) (n &>= k) 的值,代碼就不貼了,還請題主自行探索。

四、矩陣快速冪

若題主想了解效率更高的演算法來處理數據量更大的情況,可以看看以下這個編程題目:

POJ - 3070 - Fibonacci

這道題目的n的範圍上限達到1,000,000,000,若要快速得出答案,則可使用矩陣乘法+快速求冪(矩陣快速冪)的方法。

以下給出矩陣快速冪的一種寫法:

#include &

const int mod = 10007; // 結果取模的大小
const int matSize = 5; // 矩陣規模 matSize * matSize

struct Mat {
int val[matSize][matSize];
void init() {
memset(val, 0, sizeof(val));
}
void set1() { // 把矩陣置為單位矩陣
for(int i = 0; i &< matSize; ++i) for(int j = 0; j &< matSize; ++j) val[i][j] = (i == j); } friend Mat operator * (Mat a, Mat b) { // 重載*號進行矩陣乘法 Mat res; res.init(); for(int i = 0; i &< matSize; ++i) { for(int k = 0; k &< matSize; ++k) { if(a.val[i][k]) { for(int j = 0; j &< matSize; ++j) { res.val[i][j] += a.val[i][k] * b.val[k][j]; res.val[i][j] %= mod; } } } } return res; } friend Mat operator ^ (Mat a, int x) { // 重載^號進行快速冪運算 Mat res; res.set1(); while(x) { if(x 1) res = res * a; a = a * a; x &>&>= 1;
}
return res;
}
};


先放代碼:

直接借用大牛的

import Data.List
import Data.Bits

fib :: Int -&> Integer
fib n = snd . foldl" fib" (1, 0) . dropWhile not $
[testBit n k | k &<- let s = bitSize n in [s-1,s-2..0]] where fib" (f, g) p | p = (f*(f+2*g), ss) | otherwise = (ss, g*(2*f-g)) where ss = f*f+g*g fibonMod n d = mod (fib n) d

測試一下:

:set +s 顯示執行時間和內存佔用

ghci&> fibonMod 10000000 10007
28
(0.20 secs, 20298700 bytes)
ghci&> fibonMod 100000000 10007
6193
(2.89 secs, 230900212 bytes)
ghci&> fibonMod 100000 10007
5675
(0.00 secs, 0 bytes)
ghci&> fibonMod 1000000 10007
114
(0.02 secs, 1209368 bytes)
ghci&> fibonMod 10000000 10007
28
(0.20 secs, 20298740 bytes)
ghci&> fibonMod 100000000 10007
6193
(2.89 secs, 230953920 bytes)
ghci&> fibonMod 1000000000 10007
7300
(34.43 secs, 2283929676 bytes)
ghci&> fibonMod 10000000000 10007
6734
(46.36 secs, 3231113156 bytes)
ghci&> fibonMod 100000000000 10007
9877
(41.68 secs, 2785967264 bytes)


看到上面有人寫了 Haskell 的代碼, 一看時間覺得怪怪的, 怎麼會那麼慢呢. 於是我試著寫了一個:

fib n m = fst $ fib" n m

fib" n m
| n == 0 = (0, 1)
| mod n 2 == 1 = (p, q)
| otherwise = (r, p)
where
(a, b) = fib" (div n 2) m
p = mod (b * b + a * a) m
q = mod (b * (2 * a + b)) m
r = mod (a * (2 * b - a)) m

測試了一下, 證明 Haskell 還是可以很快的.

*Main&> map (flip fib 10007) [1..20]
[1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765]
(0.01 secs, 2077504 bytes)
*Main&> fib 1000000 10007
114
(0.01 secs, 1553336 bytes)
*Main&> fib 10000000 10007
28
(0.00 secs, 1556312 bytes)
*Main&> fib 100000000 10007
6193
(0.01 secs, 1556220 bytes)
*Main&> fib 1000000000000000000000000000000000000000000000000000000 10007
6193
(0.01 secs, 1555316 bytes)
*Main&> fib 1000000000000000000000000000000000000000000000000000000 10007
6193
(0.01 secs, 1554428 bytes)
*Main&> fib 10000000000000000000000000000000000000000000000000000000 10007
7300
(0.01 secs, 2098484 bytes)

這個方法是我在一次 Codeforces 比賽中看被人源代碼看到的, 原代碼只有4行:

def recfib(n,m):
if n == 0: return (0, 1)
a, b = recfib(n / 2,m)
return ((b*b+a*a)%m, b*(2*a+b)%m) if n%2 else (a*((2*b)-a)%m, ((b*b+a*a))%m)


對 @沈周 的答案做個筆記。

1. 求菲波那契數列的第n項問題可以轉換為求矩陣的冪

2. 矩陣乘法滿足結合律,因此矩陣的冪次計算可以二分

3. 可以二分且每層的計算複雜度為常量的問題,總複雜度為O(logn)


1000000爆棧了。請參考樓上的寫法。或者開更大的棧。

如果一定要遞歸的話……OJ常用開棧技巧。

#include &
#include &
const int MaxN = 1000000;
int fib[MaxN + 1];

int f(int x){
if (x == 1 || x == 2) return 1;
if (fib[x] != -1) return fib[x];
return fib[x] = (f(x - 1) + f(x - 2)) % 10007;
}

int main(){
for (int i = 1; i &<= MaxN; i++) fib[i] = -1; static int stack[MaxN * 5], bak; asm __volatile__ ( "movl %%esp, %0 " "movl %1, %%esp ": "=g"(bak): "g"(stack + MaxN * 5 - 1): ); printf("%d ", f(MaxN)); asm __volatile__ ( "movl %0, %%esp " : : "g"(bak) ); return 0; }


輪哥代碼中加一句,否則溢出

for(int i=3;i&<=n;i++) { int c=a+b; c=c%10007 a=b; b=c; }


The Fibonacci sequence

Haskell的一些演算法,其中還有Constant-time(有條件)的演算法……


int f(int n)
{
if(n&<3) return 1; int a=1; int b=1; for(int i=3;i&<=n;i++) { int c=a+b; a=b; b=c; } return b; }


寫成遞歸也沒問題,但是為了避免棧溢出,一定是要編譯器支持的尾遞歸形式,比如下面這樣寫gcc O2可以轉成循環

uint64_t fib_helper(uint64_t n,uint64_t a,uint64_t b)
{
if(n &< 3) return b; return fib_helper(n-1,b,a+b); }


快速冪方法受教了。

數學上斐波那契數列的通項公式也早就被推導出來。

a = sqrt(5);

f(n) = round(((1+a)^n-(1-a)^n)/2^n/a);


我的遞歸寫法:

#include &
#include &

#define DIV 10007

typedef
struct
{
int item ;
int remainder ;
}
F_R_t;

typedef
struct
{
F_R_t (*array_fr)[2] ;
size_t size ;
}
S_List_t;

int prepare( S_List_t * , int );
void get_size( size_t * , int );
void init( S_List_t * );
F_R_t f2n( F_R_t (*)[2] );
F_R_t f2n_1( F_R_t (*)[2] );
int get_remainder( int , S_List_t const * );
int get_sub( int , S_List_t const * );

int main( void )
{
int m ;
S_List_t s_l ;

puts("第幾項?");
scanf("%d",m);

if ( prepare( s_l , m ) == -1 )
exit(1);
printf("餘數為%d
" , get_remainder( m , s_l ) );
free(s_l.array_fr);

return 0;
}

int get_sub(int m , S_List_t const * p_l)
{
int i = 0 ;
while ( 2 * p_l-&> array_fr[i][0].item &< m ) { i ++ ; } return i; } int get_remainder( int m , S_List_t const * p_l ) { int sub = get_sub( m , p_l ); if ( m == 0 ) return 0 % DIV ; if ( m == 1 || m == 2 ) return 1 % DIV ; if ( p_l-&>array_fr[sub][0].item == m )
return p_l-&>array_fr[sub][0].remainder ;

if ( p_l-&>array_fr[sub][1].item == m )
return p_l-&>array_fr[sub][1].remainder ;

return (
get_remainder( m - p_l-&>array_fr[sub][0].item , p_l ) * p_l-&>array_fr[sub][1].remainder % DIV
+ get_remainder( m - p_l-&>array_fr[sub][1].item , p_l ) * p_l-&>array_fr[sub][0].remainder % DIV
) %DIV ;
}

void get_size( size_t * p , int m )
{
* p = 1u ;
m -- ;
while( (m /= 2) &> 0 )
{
++ * p ;
}
}

F_R_t f2n( F_R_t ( * p )[2] )
{
F_R_t tmp ;
tmp.item = (*p)-&>item * 2 ;
tmp.remainder
= (*p)-&>remainder * ( *p + 1 )-&>remainder % DIV * 2 % DIV
- (*p)-&>remainder * (*p)-&>remainder % DIV ;
tmp.remainder += DIV ;
tmp.remainder %= DIV ;
return tmp;
}

F_R_t f2n_1( F_R_t ( * p )[2] )
{
F_R_t tmp ;
tmp.item = (*p)-&>item * 2 + 1 ;
tmp.remainder
= (*p+1)-&>remainder * ( *p + 1 )-&>remainder % DIV +
(*p)-&>remainder * (*p)-&>remainder % DIV ;
tmp.remainder %= DIV ;
return tmp;
}

void init( S_List_t * p_l )
{
size_t i ;

p_l-&>array_fr[0][0] = ( F_R_t ){ 1 , 1 % DIV } ; //C99用法
p_l-&>array_fr[0][1] = ( F_R_t ){ 2 , 1 % DIV } ; //C99用法

for ( i = 1u ; i &< p_l -&>size ; i++)
{
p_l-&>array_fr[i][0] = f2n( p_l-&>array_fr + i - 1 ) ;
p_l-&>array_fr[i][1] = f2n_1( p_l-&>array_fr + i - 1 ) ;
}
}

int prepare( S_List_t * p_l , int m )
{
get_size( p_l-&>size , m ) ;

p_l -&> array_fr = calloc( sizeof(F_R_t [2]) , p_l -&> size ) ;

if ( p_l -&> array_fr == NULL )
return -1;

init( p_l );
}

沒太留心運行時間,不過應該還不至於讓樓主拿1000000測試就直接跪。

在我發表的代碼中,這個應該算最丑的,請大家原諒。以後有空我會改好點。

之所以拿出來獻醜,只是想說明這個問題可以用遞歸。


貌似2年前做的ACM題!!!既然答案都被 @李鵬給了~~那麼我就不給代碼了吧。

思路差不多這樣:return f(n-1)+f(n-2)的時間複雜度顯然吃不消!所以我們得做類似於Memoize的東西,這裡叫做打表!做ACM好多題都可以打表,所以代碼可以這麼寫:

#include &
using namespace std;
long long a[1000000];
void fib()
{
a[0] = 0;
a[1] = a[2] = 1;
for (int i = 3; i &< 1000000; i++) { a[i] = a[i-1] + a[i-2] ;//全部都存在了a這個數組中,待會直接取 //如果求餘數,這句改為a[i]=(a[i-1] + a[i-2])%10007 //此時你基本可AC了。 } } int main() { int n; fib(); cin &>&> n;
cout &<&< a[n] &<&< endl; return 0; }

然後你可以做其他的~~

如果只是求簡單的遞歸,那麼方式很多:

  • return f(n-1)+f(n-2);//不推薦
  • 這裡會考慮到有沒有叫做尾遞歸優化的東西~~Python自帶沒有。

  • 利用fib的公式求解
  • 迭代。上面那些回答的人中就有迭代。
  • 呵呵~~~


直接跪是指什麼?如果是指TLE的話很正常,斐波那契數列用遞歸寫效率非常低。


你可以查查斐波那契的黃金分割通項公式或者利用變換族的對數求解步驟 對數步計算斐波那契數列 這個應該寫起來最簡單


這麼大的數據,目測是stackoverflow了。別用遞歸。


@沈周 給出的應該是目前答案中最優化的解 我也隨便寫了使用迭代和矩陣迭代的版本以及性能對比 希望有所幫助:

https://gist.github.com/yzhwang/9225019

如果n為1000000000,也就是1 billion的話 基本上性能差距達到了600000倍 很可觀


扯一句沒用的

樓上的方法大都可以優化

幹嘛要用取余這麼慢的操作呢?

---------------------分割線-------------------------------------

上一下代碼吧

#include &
#include &
#include &
#define DIV_N 10007
int
fib_div(int n)
{
int f1 = 1,f2 = 1;
int i;
int res = 0;
int tmp;
if (n &< 3) { return 1; } for(i = 2;i &< n; i++) { res = f1 + f2; f1 = f2; f2 = res; if(f1 &> DIV_N) {
f1 -= DIV_N;
}
if(f2 &> DIV_N) {
f2 -= DIV_N;
}
if (f1 == 1 f2 == 1) {
tmp = n % (i - 1);
return fib_div(tmp);// can be optimized
}
}
return f2;
}
int main()
{
int n = 1000000000;
int rv;
clock_t f_s,f_e;
f_s=clock();
rv = fib_div(n);
f_e=clock();
printf("So n is %d, and the result is %d, time %ums
",n, rv, f_e-f_s);
return 0;
}

結果

So n is 1000000000, and the result is 7300, time 0ms

機器是11年的老機器。


編程之美 P162可以得到很好的思路


直接用 f(n)=1/√5*((1+√5)/2)^n 一步出來,推導用homogeneous 遞歸公式,保證不會爆,速度杠杠的


推薦閱讀:

C語言可否自定義數值類型(或是任意個位元組的數值類型)?
利用異或方式交換兩個變數值的原理是什麼?
為什麼計算機里加上存儲功能可以代替改變電路?
為什麼開源軟體絕大部分都是C語言寫的,而商業軟體大多數是C++開發的?
成為一個優秀的程序員,一定要精通C/C++嗎?

TAG:C編程語言 | 演算法設計 |