记一次与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思路应该是:
- 使用迭代而非递归(O(n)时间)
- 每次加法后立即取模(防止溢出)
《每次加法后立即取模》
也就是说一开始我们的分析中就错了一步,斐波那契数列增长速度极快,也就是说无论我们如何去补救,如果始终采取先求数再取模的话就会导致TLE!
所以真正的思路是这样的:
- 递归方法的问题:直接使用递归计算Fibonacci数列会导致指数级的时间复杂度(O(2^n)),当n较大时(如n=50),甚至不需要等到1000000就会使计算非常缓慢甚至栈溢出。
- 迭代方法:使用循环从F1和F2逐步计算到Fn,时间复杂度为O(n),适用于较大的n值。
- 模运算性质:由于我们只需要结果对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;
}
总结
- 看清数据范围
- 用迭代而不是递归
- 无需求出对应数而是每一步都进行取模
- 有一颗宁静的心