记一次与Fibonacci斗智斗勇

记一次与Fibonacci斗智斗勇

楔子:

事必有因。近时方备战粤港澳信息学竞赛,适值主办方有其平台,故欲以其平台练题。然用之颇觉不堪,譬如一题所予提示甚少,题目分类未明,且大多无题解。此题吾亦屡试方解出,遂作此文,以便后学及自我温习。

那首先来看看这题目和出题人究竟时何方神圣

题目描述:

LQ1005:入门训练Fibonacci数列

知识点:简单递归 C++-运算符和表达式概念

Fibonacci数列的递推公式为:Fn=Fn-1+Fn-2,其中F1=F2=1。
当n比较大时,Fn也非常大,现在我们想知道,Fn除以10007的余数是多少。

输入格式

输入描述:
输入包含一个整数n
输入样例:

10

输出格式

输出描述:
输出一行,包含一个整数,表示Fn除以10007的余数。

说明:在本题中,答案是要求Fn除以10007的余数,因此我们只要能算出这个余数即可,而不需要先计算出Fn的准确值,再将计算的结果除以10007取余 (记住这里后面要考)
数,直接计算余数往往比先算出原数再取余简单。
输出样例:

55

【提升】
HINT:时间限制:1.0s 内存限制:256.0MB
1≤n ≤ 1,000,000.

分析

初步分析

我们从题目中可以得知,这道题要求我们实现以下两个部分

  • 先实现求出斐波那契数列
  • 再用所求出的数进行模10007后输出值

那这样的话,我们的思路一般都是一眼丁真 一眼递归,那么我们可以写出如下代码:

#include <iostream>
using namespace std;

int Fib(int n) {
    if(n == 0) return 0;
    if(n == 1) return 1;
    else return Fib(n-1)+Fib(n-2);
}

int main() {
    int n;
    cin >> n;
    cout << Fib(n) << endl;
    return 0;
}

之后我们对其进行补充,加上让它模掉10007就可以了,prefect~

like this(在这里水一波字数)

#include <iostream>
using namespace std;

int Fib(int n) {
    if(n == 0) return 0;
    if(n == 1) return 1;
    else return Fib(n-1)+Fib(n-2);
}

int main() {
    int n;
    cin >> n;
    int ans = Fib(n)%10007;
    cout << ans << endl;
    return 0;
}

根据我们的值这道题没有问题,不用2分钟就AC了,然而事情真就如此吗?

恰恰相反 实则不然 人之常情(bushi)

当我把题解放上去满心憧憬的时候

爆零了

!?

解剖开始

我们重新回到题目描述中,可以看到它的数据范围为1≤n ≤ 1,000,000,那如果我们直接输入1,000,000那就会。。。

➜  p1706 git:(master) ✗ ./a.out
1000000
[1]    59990 segmentation fault (core dumped)  ./a.out

就会趋势

那既然超出了数值范围,那我们改成longlong试试

我们只得了300分。。。

也就是说我们是没有完全AC的,看了扣分点提示我们TLE了,根据目前的代码想想是哪里出了问题呢?

#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;
    long long a = 1, b = 1, c;  
    for (int i = 3; i <= n; i++) {
        c = a + b; 
        a = b;
        b = c;
    }
    cout << b % 10007 << endl;
    return 0;
}

输出内容是这样的

➜  p1706 git:(master) ✗ ./a.out     
1000000
-1044

我们能明显的意识到它发生整数溢出了,是计算机中整数存储的典型表现:当数值超过数据类型能表示的最大正值时,会”环绕”到负值范围。但是明明我们都用上了long long了,但是为什么还发生了溢出?

那我们加大剂量

#include <iostream>
using namespace std;
const int MOD = 1e9 +7; // 防止数值溢出

struct Matrix {
    long long mat[2][2];
    Matrix() { mat[0][0]=mat[0][1]=mat[1][0]=1; mat[1][1]=0; }
};

Matrix multiply(const Matrix& a, const Matrix& b) {
    Matrix res;
    res.mat[0][0] = (a.mat[0][0]*b.mat[0][0] + a.mat[0][1]*b.mat[1][0]) % MOD;
    res.mat[0][1] = (a.mat[0][0]*b.mat[0][1] + a.mat[0][1]*b.mat[1][1]) % MOD;
    res.mat[1][0] = (a.mat[1][0]*b.mat[0][0] + a.mat[1][1]*b.mat[1][0]) % MOD;
    res.mat[1][1] = (a.mat[1][0]*b.mat[0][1] + a.mat[1][1]*b.mat[1][1]) % MOD;
    return res;
}

Matrix matrix_pow(Matrix a, int n) {
    Matrix res;
    res.mat[0][0] = res.mat[1][1] = 1; // 初始化为单位矩阵
    res.mat[0][1] = res.mat[1][0] = 0;
    
    while(n > 0) {
        if(n & 1) res = multiply(res, a);
        a = multiply(a, a);
        n >>= 1;
    }
    return res;
}

long long fib(int n) {
    if(n == 0) return 0;
    Matrix m;
    m = matrix_pow(m, n-1);
    return m.mat[0][0];
}

int main() {
    int n;
    cin >> n;
    cout << fib(n)%10007 << endl;
    return 0;
}

然后还是300分,原因。。。0Wrong AnswerRead 3057, expect 2091.

那我们再改改

#include <iostream>
using namespace std;

long long fib(int n) {
    if (n == 0) return 0;
    long long a = 0, b = 1, c;
    for (int i = 2; i <= n; i++) {
        c = a + b;
        a = b;
        b = c;
    }
    return b;
}

int main() {
    int n;
    cin >> n;
    int ans = fib(n)%10007;
    cout << ans << endl;
    return 0;
}

好了,这次多了100分,看一眼报错0Wrong Answer Read 4432, expect 6545.

再看看题目描述呢?

摘要

输出描述:
输出一行,包含一个整数,表示Fn除以10007的余数。

说明:在本题中,答案是要求Fn除以10007的余数,因此我们只要能算出这个余数即可,而不需要先计算出Fn的准确值,再将计算的结果除以10007取余数,直接计算余数往往比先算出原数再取余简单。

在根据我们前面的尝试,得出这道题的AK思路应该是:

  1. 使用迭代而非递归(O(n)时间)
  2. 每次加法后立即取模(防止溢出)

《每次加法后立即取模》

也就是说一开始我们的分析中就错了一步,斐波那契数列增长速度极快,也就是说无论我们如何去补救,如果始终采取先求数再取模的话就会导致TLE!

所以真正的思路是这样的:

  1. 递归方法的问题:直接使用递归计算Fibonacci数列会导致指数级的时间复杂度(O(2^n)),当n较大时(如n=50),甚至不需要等到1000000就会使计算非常缓慢甚至栈溢出。
  2. 迭代方法:使用循环从F1和F2逐步计算到Fn,时间复杂度为O(n),适用于较大的n值。
  3. 模运算性质:由于我们只需要结果对10007的余数,可以在每一步计算时都对10007取模,利用模运算的性质((a + b) mod m = (a mod m + b mod m) mod m)防止数值溢出。

那既然思路清楚了,我们可以给出真正的AC代码了!

#include <iostream>
using namespace std;

int fibMod(int n) {
    const int MOD = 10007;
    if (n == 0) return 0;
    if (n == 1) return 1;
    
    int a = 0, b = 1, c;
    for (int i = 2; i <= n; ++i) {
        c = (a + b) % MOD;  // 每一步计算都取模防止溢出
        a = b;
        b = c;
    }
    return b;
}

int main() {
    int n;
    cin >> n;
    cout << fibMod(n)%10007 << endl;
    return 0;
}

总结

  • 看清数据范围
  • 用迭代而不是递归
  • 无需求出对应数而是每一步都进行取模
  • 有一颗宁静的心

记一次与Fibonacci斗智斗勇
https://p4y1oad.github.io/2025/05/17/记一次与Fibonacci斗智斗勇/
Author
Hailin Zheng
Posted on
May 17, 2025
Licensed under