最近系里又开始了蓝桥杯大赛的培训工作,一群飞蛾扑火般的大一菜鸟正在被折磨得死去活来。这里就来一道基础题下下饭吧。
问题描述
Fibonacci数列的递推公式为:Fn=Fn-1+Fn-2,其中F1=F2=1。
当n比较大时,Fn也非常大,现在我们想知道,Fn除以10007的余数是多少。
输入格式
输入包含一个整数n。
输出格式
输出一行,包含一个整数,表示Fn除以10007的余数。
说明:在本题中,答案是要求Fn除以10007的余数,因此我们只要能算出这个余数即可,而不需要先计算出Fn的准确值,再将计算的结果除以10007取余数,直接计算余数往往比先算出原数再取余简单。
样例输入
10
样例输出
55
样例输入
22
样例输出
7704
数据规模与约定
1 <= n <= 1,000,000
题目解析
斐波那契数列大家应该在以前的学习过程中有所了解,没有了解也没关系。
斐波那契数列的基本概念就是 除前两项外,数列的每一项都是前两项之和,前两项为1。
由定义可以得到递推公式:
KaTeX parse error: Undefined control sequence: \ce at position 1: \̲c̲e̲{F[n] = F[n - 1…
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 ……
题中要求给出对10007取模(余数)的结果。以数列第22项10946为例,给出的结果应该是17711%10007=7704
。
然后看一看数据规模,好家伙,可能要算到第1,000,000
项。
在此提醒,算法题的数据规模部分非常重要,它直接决定了题目的难度。
当然,你可能对斐波那契数列第1,000,000项没什么概念,那么我告诉你这是一个208988位的数。用A4纸五号字需要61页纸。
开玩笑,C语言的int类型最大是2147483647,long long最大是9.223372037×10¹⁸,这怎么可能把结果算出来。
别着急,题干中的说明给了你思路
说明:在本题中,答案是要求Fn除以10007的余数,因此我们只要能算出这个余数即可,而不需要先计算出Fn的准确值,再将计算的结果除以10007取余数,直接计算余数往往比先算出原数再取余简单。
什么意思呢,让我们来看两组算式:
6765 + 10946 = 17711 17711 % 10007 = 7704 10946 % 10007 = 939 6765 + 939 = 7704
28657 + 46368 = 75025 75025 % 10007 = 4976 28657 % 10007 = 8643 46368 % 10007 = 6340 8643 + 6340 = 14983 14983 % 10007 = 4976
可以看出,
两数相加后对10007的模
和
两加数对10007的模的和对10007的模
是完全一致的。有点拗口,慢慢看。
所以我们并不需要算出结果后再取模,同时将每次计算的结果都限制到了10007以内。这样原本巨大的一个数我们现在可以用int
就能解决了。
递归解法
递归是非常基础的算法逻辑,这是百度对递归的解释
程序调用自身的编程技巧称为递归( recursion)。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回
——百度百科
说了半天没什么意思,以斐波那契数列举例,这里用F[n]表示,
因为 除前两项外,数列的每一项都是前两项之和,前两项为1,所以
F[5]=F[3]+F[4]
,其中
F[3]=F[1]+F[2]
,
F[4]=F[2]+F[3]
,
那么F[5]
可以前两项表示写成
F[5]=(F[1]+F[2])+(F[2]+(F[1]+F[2]))
。
这就是递归,就是函数自己调用自己,和数列递推公式是不是异曲同工。
现在我们想一想如果数列的n不需要从0开始,并且没有除了前两项外的限制,那么这个式子
KaTeX parse error: Undefined control sequence: \ce at position 1: \̲c̲e̲{F[n] = F[n - 1…
将会无穷无尽地递归下去。
所以必须由一个条件限制着,使它不会无穷无尽地递归下去,也就是这个除前两项外……前两项为1,在递归中,我们称之为边界条件。
那我们现在根据上面说的一大堆写一下递归函数。
首先,函数应该描述清楚递推公式,很容易写出
int calFib(int n)
{
//用于暂存运算结果
int res = 0;
//将公式表示出来
res = calFib(n - 1) + calFib(n - 2);
return res;
}
然后加入边界条件,也就是数列第1、2项即n为1、2的时候应该为1
int calFib(int n)
{
int res = 0;
//边界条件,当前两项为1
if(n == 1 || n == 2)
{
res = 1;
}
//将公式表示出来
res = calFib(n - 1) + calFib(n - 2);
return res;
}
运算时对10007取模,
int calFib(int n)
{
int res = 0;
//边界条件,当前两项为1
if(n == 1 || n == 2)
{
res = 1;
}
//将公式表示出来
//对10007取模后相加
res = calFib(n - 1) % 10007 + calFib(n - 2) % 10007;
//对相加的结果再取模
return res % 10007;
}
简化代码并加入主函数
#include <stdio.h>
int calFib(int n)
{
//边界条件,当前两项为1
if(n == 1 || n == 2)
{
return 1;
}
//将公式表示出来
//对10007取模后相加的和取模
return (calFib(n - 1) % 10007 + calFib(n - 2) % 10007) % 10007;
}
int main(void)
{
int n = 0;
scanf("%d", &n);
printf("%d", calFib(n));
return 0;
}
在这里提醒一下
所有算法题的主函数必须再程序结束时返回0
return 0;
否则将会认为运行错误导致一分没有
简单循环解法
实际上,递归虽然是斐波那契数列的典型解法,但是这只能说明用斐波那契数列讲递归比较简单,并不能说明递归就是斐波那契数列的最佳解法。未经性能优化的递归算法的时间复杂度和空间复杂度都非常高,也就是效率低得出奇。尝试过提交上述递归代码的同学可能发现,递归方法只能解决前三个测试用例,剩下的均提示运行超时。所以这里再讲解一下用简单的循环实现斐波那契数列递推的方法。
对于这个问题,我们可以将其分解为三部分,分别为:
- 输入部分
- 计算部分
- 输出部分
我们写出一个程序框架如下
#include <stdio.h>
int main(void)
{
//1. 输入部分
//2. 计算部分
//3. 输出部分
return0;
}
另外,我们还需要声明一些变量用来计算和保存计算中间值。
因为斐波那契数列用递推公式定义,所以我们可以从第三项开始一个一个往后计算,总能得到所有我们想要的。另外在斐波那契数列的递推公式中,实际参与计算的只有三个数,这一项、上一项、上上项,所以我们只需要三个变量参与计算就可以,因为计算只需要从第三项开始所以在一开始认为上一项和上上项都是1。
#include <stdio.h>
int main(void)
{
//我选择声明一个数组来得到三个变量。
//分别用a[0]表示上上项,a[1]表示上一项,a[2]表示这一项。
//因为计算只需要从第三项开始所以在一开始认为上一项和上上项都是1。
int a[3] = {1, 1, 0};
//再声明两个变量用于承接输入和输出
int n = 0, res = 0;
//还有一个变量用做循环变量
int i = 0;
//1. 输入部分
//2. 计算部分
//3. 输出部分
return 0;
}
输入部分就只是输入一个n而已,输出部分也只是一个数而已。
#include <stdio.h>
int main(void)
{
//我选择声明一个数组来得到三个变量。
//分别用a[0]表示上上项,a[1]表示上一项,a[2]表示这一项。
//因为计算只需要从第三项开始所以在一开始认为上一项和上上项都是1。
int a[3] = {1, 1, 0};
//再声明两个变量用于承接输入和输出
int n = 0, res = 0;
//还有一个变量用做循环变量
int i = 0;
//1. 输入部分
scanf("%d", &n);
//2. 计算部分
//3. 输出部分
printf("%d", res);
return 0;
}
核心来了,在计算部分应该加入除前两项外,也就是第三项之后每一项的计算,并且当输出n为1或2时不需要计算直接输出1。
#include <stdio.h>
int main(void)
{
//我选择声明一个数组来得到三个变量。
//分别用a[0]表示上上项,a[1]表示上一项,a[2]表示这一项。
//因为计算只需要从第三项开始所以在一开始认为上一项和上上项都是1。
int a[3] = {1, 1, 0};
//再声明两个变量用于承接输入和计算结果
int n = 0, res = 0;
//还有一个变量用做循环变量
int i = 0;
//1. 输入部分
scanf("%d", &n);
//2. 计算部分
if(n == 1 || n == 2) //前两项直接输出1
{
res = 1;
}
else
{
for(i = 3; i <= n; i++)
{
//这一项 = 上一项 + 上上项
a[2] = a[1] + a[0];
//循环的下一次计算时,上一项时这一次计算的这一项,上上项时这一次计算的上一项
//为了方便下一次计算,将上一项赋给上上项,将这一项赋给上一项。
a[0] = a[1];
a[1] = a[2];
}
//计算到第n项之后将计算结果进行输出。
res = a[2];
}
//3. 输出部分
printf("%d", res);
return 0;
}
加入取模并简化代码
#include <stdio.h>
int main(void)
{
int a[3] = {1, 1, 0};
int n = 0;
int i = 0;
scanf("%d", &n);
if(n == 1 || n == 2)
{
printf("%d", 1);
}
for(i = 3; i <= n; i++)
{
a[2] = a[1] % 10007 + a[0] % 10007;
a[0] = a[1];
a[1] = a[2];
}
printf("%d", a[2] % 10007);
return 0;
}
齐活。