csp信奥赛c++中的递归和递推研究
通过斐波那契数列理解递归与递推
题目描述
斐波那契数列是指这样的数列:数列的第一个和第二个数都为 1 1 1,接下来每个数都等于前面 2 2 2 个数之和。
给出一个正整数 a a a,要求斐波那契数列中第 a a a 个数是多少。
输入格式
第 1 1 1 行是测试数据的组数 n n n,后面跟着 n n n 行输入。每组测试数据占 1 1 1 行,包括一个正整数 a a a( 1 ≤ a ≤ 30 1 \le a \le 30 1≤a≤30)。
输出格式
输出有 n n n 行,每行输出对应一个输入。输出应是一个正整数,为斐波那契数列中第 a a a 个数的大小。
输入输出样例 1
输入 1
4
5
2
19
1
输出 1
5
1
4181
1
通过本题来深入研究递归和递推的思想
斐波那契数列的定义是:
F(1)=1, F(2)=1, F(n)=F(n-1)+F(n-2) ( n ≥ 3 n\ge 3 n≥3) 。
这个定义本身就是递归 的------用自身更小的实例来定义自身。而递推则是从已知的基础值出发,一步步推出更大的值。
下面通过这道题来深入理解两种思想。
一、递归
递归是一种直接或间接调用自身的函数设计方法。它把一个大型复杂问题层层转化为一个规模较小的同类问题,直到达到可以直接求解的"基础情况"(递归基)。
对应代码(递归版):
cpp
int fib(int a) {
if (a == 1 || a == 2) return 1; // 递归基
return fib(a - 1) + fib(a - 2); // 递归调用
}
执行过程(以 fib(5) 为例):递归树
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ | | |
fib(2) fib(1) 1 1 1
| |
1 1
树的说明:
- 每个结点代表一次函数调用,结点下的两个子结点是它递归调用的两个子问题。
- 叶子结点是递归基
fib(1)或fib(2),直接返回 1。 - 从叶子开始向上逐层返回:
fib(2)=1,fib(1)=1→fib(3)=1+1=2→fib(4)=fib(3)+fib(2)=2+1=3→fib(5)=fib(4)+fib(3)=3+2=5。
重复计算问题:
- 观察树中,
fib(3)被计算了 2 次 (分别在fib(5)的左子树和右子树中),fib(2)被计算了 3 次。 - 当
a增大时,重复计算的结点数呈指数增长,导致总调用次数约为 O ( 2 n ) O(2^n) O(2n)。例如a=30时调用次数超过 270 万次,效率极低。
特点:
- 思路自然:直接翻译数学定义,代码简洁易写。
- 缺点明显:大量重复计算,效率低下,且递归深度过大可能导致栈溢出。
- 适用场景:问题规模很小,或者配合记忆化优化(见后文记忆化搜索)。
二、递推
递推是从已知的初始条件出发,按照某种规则逐步推导出后续所有结果。它通常使用循环(迭代)实现,是一种自底向上的计算方法。
对应代码(递推预存储版):
cpp
int f[31];
f[1] = f[2] = 1; // 初始条件
for (int i = 3; i <= 30; ++i) {
f[i] = f[i-1] + f[i-2]; // 递推公式
}
// 查询时直接返回 f[a]
执行过程(计算 f[3] 到 f[5]):
f[3] = f[2] + f[1] = 1+1 = 2
f[4] = f[3] + f[2] = 2+1 = 3
f[5] = f[4] + f[3] = 3+2 = 5
每一步只用前面已经算好的值,没有重复计算。
特点:
- 效率高 :计算
f[n]只需 O(n) 时间,查询只需 O(1)(若预存)。 - 空间可控:可以只保留最近两项,空间 O(1);也可以预存整个表,空间 O(n) 但查询更快。
- 无函数调用开销:纯循环实现,不会栈溢出。
- 思路要求:需要找到"如何从已知推未知"的规则,不如递归直观。
适用场景:
- 问题具有明确的阶段性和递推关系(如动态规划、数列计算)。
- 需要反复查询不同 n 的结果(预存后 O(1) 查询)。
- 大规模数据(如 n= 10 6 10^6 106 甚至更大)。
三、递归 vs 递推 对比表
| 对比维度 | 递归 | 递推 |
|---|---|---|
| 实现方式 | 函数调用自身 | 循环迭代 |
| 方向 | 自顶向下(从大问题分解到小问题) | 自底向上(从小问题构建大问题) |
| 重复计算 | 严重(未优化时) | 无 |
| 时间复杂度 | 指数级 O ( 2 n ) O(2^n) O(2n) | 多项式级O(n) |
| 空间复杂度 | 调用栈深度 O(n) | 通常 O(1) 或 O(n) |
| 代码可读性 | 贴近数学定义,直观 | 需要设计迭代顺序,稍不直观 |
| 风险 | 栈溢出风险(n 大时) | 基本无风险 |
四、本题方法思路
本题中 a ≤ 30,三种方法(递归、记忆化搜索、递推)都能在时限内通过。
- 纯粹递归:虽然能 AC,但效率最低,仅适合理解定义或极小的 n。
- 递推(预存储):代码清晰、查询最快,适合工程实践。
- 记忆化搜索:是递归的优化版,保留了递归的直观形式,同时消除了重复计算,适合那些"不好直接写出递推顺序但容易递归"的问题(如图论中的 DAG 记忆化搜索)。
总结一句话:
递归是"自己调用自己",分解问题;递推是"从已知推未知",迭代求解。递归自然但低效,递推高效但需思考顺序。
解法一:递归
思路
直接按照斐波那契的数学定义编写递归函数:
- 终止条件:
a == 1 || a == 2返回 1 - 否则返回
fib(a-1) + fib(a-2)
时间复杂度 O( 2 n 2^n 2n),空间复杂度 O(n)(调用栈深度)。
代码实现
cpp
#include <bits/stdc++.h>
using namespace std;
// 递归计算斐波那契数列的第 a 项
int fib(int a) {
if (a == 1 || a == 2) return 1; // 边界
return fib(a - 1) + fib(a - 2); // 递归调用
}
int main() {
int n, a;
cin >> n;
while (n--) {
cin >> a;
cout << fib(a) << endl;// 直接输出递归结果
}
return 0;
}
解法二:记忆化搜索
思路
使用一个全局数组 memo 保存已计算过的值。
在 fib 函数中:
- 如果
memo[a] != 0直接返回 - 否则递归计算并存入
memo[a]
时间复杂度 O(n),空间复杂度 O(n)。
对多组测试数据,memo 复用,效率高。
代码
cpp
#include <bits/stdc++.h>
const int MAXN = 31;
int memo[MAXN]; //记忆化数组,0表示未计算
// 记忆化搜索计算斐波那契数列的第 a 项
int fib(int a) {
if (a == 1 || a == 2) return 1; // 边界
if (memo[a] != 0) return memo[a]; // 已计算,直接返回
memo[a] = fib(a - 1) + fib(a - 2); // 计算并保存
return memo[a];
}
int main() {
memset(memo, 0, sizeof(memo)); // 初始化记忆数组
int n, a;
cin >> n;
while (n--) {
cin >> a;
cout << fib(a) << endl;
}
return 0;
}
解法三:递推
思路
提前计算出所有可能用到的斐波那契数(因为 a≤30,所以计算到 30 即可)。
用一个全局数组 f 存储,f[1] = f[2] = 1,然后 for i = 3 to 30: f[i] = f[i-1] + f[i-2]。
函数 fib(int a) 直接返回 f[a]。
这样对于每个查询只需 O(1) 时间,预处理 O(n)。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 31;
int f[MAXN]; // 存储斐波那契数列的数组
int main() {
// 预处理:计算 f[1] 到 f[30]
f[1] = f[2] = 1;
for (int i = 3; i < MAXN; ++i) {
f[i] = f[i - 1] + f[i - 2];
}
int n, a;
cin >> n;
while (n--) {
cin >> a;
cout << f[a]<< endl; // O(1) 输出
}
return 0;
}
三种解法对比总结
| 方法 | 时间复杂度(单次查询) | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 递归 | O( 2 n 2^n 2n) | O(n) | 代码极简,与数学定义完全一致 | 重复计算极多,n=30 时调用次数约 270 万次,效率低 |
| 记忆化搜索 | O(n) | O(n) | 保留递归的直观性,避免重复计算,可复用中间结果 | 递归深度仍为 n(n≤30 安全),需要额外数组 |
| 递推+预存储 | O(1) | O(n) | 查询最快,预处理后直接数组访问,适合多组查询 | 需要事先知道最大范围(本题已知,无影响) |
针对本题(多组查询, a ≤ 30 a \le 30 a≤30)
- 递推+预存储 是最优选择,查询时间复杂度最低,代码也简洁。
- 记忆化搜索在本题也能高效运行,且不需要提前预知最大 n,但每次查询仍可能触发递归调用。
- 纯递归效率最低。
推荐:在实际编程中,对于固定小范围的多组查询,预存储法最佳。
各种学习资料,助力大家一站式学习和提升!!!
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"########## 一站式掌握信奥赛知识! ##########";
cout<<"############# 冲刺信奥赛拿奖! #############";
cout<<"###### 课程购买后永久学习,不受限制! ######";
return 0;
}
【秘籍汇总】(完整csp信奥赛C++学习资料):
1、csp/信奥赛C++,完整信奥赛系列课程(永久学习):
https://edu.csdn.net/lecturer/7901 点击跳转

2、CSP信奥赛C++竞赛拿奖视频课:
https://edu.csdn.net/course/detail/40437 点击跳转

https://edu.csdn.net/course/detail/41081 点击跳转

3、csp信奥赛高频考点知识详解及案例实践:
CSP信奥赛C++动态规划:
https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转
CSP信奥赛C++标准模板库STL:
https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转
信奥赛C++提高组csp-s知识详解及案例实践:
https://blog.csdn.net/weixin_66461496/category_13113932.html 点击跳转
4、csp信奥赛冲刺一等奖有效刷题题解:
CSP信奥赛C++初赛及复赛高频考点真题解析(持续更新): https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转
信奥赛C++提高组csp-s初赛&复赛真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13125089.html 点击跳转
5、GESP C++考级真题题解:

GESP(C++ 一级+二级+三级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转

GESP(C++ 四级+五级+六级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转

GESP(C++ 七级+八级)真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13117178.html 点击跳转
· 文末祝福 ·
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"跟着王老师一起学习信奥赛C++";
cout<<" 成就更好的自己! ";
cout<<" csp信奥赛一等奖属于你! ";
return 0;
}
