记一次与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后输出值

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

C++ 复制代码
#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(在这里水一波字数)

C++ 复制代码
#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那就会。。。

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

就会趋势

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

我们只得了300分。。。

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

C++ 复制代码
#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;
}

输出内容是这样的

zsh 复制代码
➜  p1706 git:(master) ✗ ./a.out     
1000000
-1044

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

那我们加大剂量

C++ 复制代码
#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.

那我们再改改

c++ 复制代码
#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代码了!

C++ 复制代码
#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;
}

总结

  • 看清数据范围
  • 用迭代而不是递归
  • 无需求出对应数而是每一步都进行取模
相关推荐
Mr_Xuhhh5 分钟前
Qt窗口(2)-工具栏
java·c语言·开发语言·数据库·c++·qt·算法
JQLvopkk23 分钟前
C#通过HslCommunication连接西门子PLC1200,并防止数据跳动的通用方法
前端·javascript·算法
艾莉丝努力练剑33 分钟前
【数据结构与算法】数据结构初阶:详解顺序表和链表(五)——双向链表
c语言·开发语言·数据结构·学习·算法
李昊哲小课43 分钟前
支持向量机SVM
人工智能·算法·机器学习·支持向量机·数据挖掘·sklearn
jndingxin44 分钟前
OpenCV多尺度图像增强算法函数BIMEF()
人工智能·opencv·算法
算法_小学生1 小时前
Hinge Loss(铰链损失函数)详解:SVM 中的关键损失函数
开发语言·人工智能·python·算法·机器学习·支持向量机
paopaokaka_luck1 小时前
基于SpringBoot+Vue的汽车租赁系统(协同过滤算法、腾讯地图API、支付宝沙盒支付、WebsSocket实时聊天、ECharts图形化分析)
vue.js·spring boot·后端·websocket·算法·汽车·echarts
kyle~1 小时前
C++---cout、cerr、clog
开发语言·c++·算法
钢铁男儿2 小时前
PyQt5信号与槽(信号与槽的高级玩法)
python·qt·算法
Aurora_wmroy3 小时前
算法竞赛备赛——【图论】求最短路径——Floyd算法
数据结构·c++·算法·蓝桥杯·图论