csp信奥赛c++中的递归和递推研究

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)=1fib(3)=1+1=2fib(4)=fib(3)+fib(2)=2+1=3fib(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;
}
相关推荐
Bczheng12 小时前
五.serialize.h中的CDataStream类
算法·哈希算法
小O的算法实验室2 小时前
2025年SEVC,考虑组件共享的装配混合流水车间批量流调度的多策略自适应差分进化算法,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
汀、人工智能2 小时前
[特殊字符] 第36课:柱状图最大矩形
数据结构·算法·数据库架构·图论·bfs·柱状图最大矩形
List<String> error_P2 小时前
蓝桥杯最后冲刺(三)
算法
样例过了就是过了2 小时前
LeetCode热题100 跳跃游戏
c++·算法·leetcode·贪心算法·动态规划
chen_ever2 小时前
从网络基础到吃透 Linux 高并发 I/O 核心(epoll+零拷贝 完整版)
linux·网络·c++·后端
无限进步_3 小时前
【C++&string】寻找字符串中第一个唯一字符:两种经典解法详解
开发语言·c++·git·算法·github·哈希算法·visual studio
FluxMelodySun3 小时前
机器学习(二十九) 稀疏表示与字典学习(LASSO算法、KSVD算法、奇异值分解)
人工智能·算法·机器学习
小此方3 小时前
Re:思考·重建·记录 现代C++ C++11篇 (二) 右值引用与移动语义&引用折叠与完美转发
开发语言·c++·c++11·现代c++