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;
}
相关推荐
数据牧羊人的成长笔记几秒前
逻辑回归与Softmax回归
算法·回归·逻辑回归
郑州光合科技余经理21 分钟前
同城O2O海外版二次开发实战:从支付网关到配送算法
开发语言·前端·后端·算法·架构·uni-app·php
张健11564096481 小时前
使用信号量限制并发数量
开发语言·c++
jc06202 小时前
6.1云原生之Docker
c++·docker·云原生
d111111111d3 小时前
STM32-UART封装问题解析
笔记·stm32·单片机·嵌入式硬件·学习·算法
叶子野格4 小时前
《C语言学习:指针》12
c语言·开发语言·c++·学习·visual studio
Jiangxl~5 小时前
IP数据云如何为不同行业提供精准IP查询与风险防控解决方案?
网络·网络协议·tcp/ip·算法·ai·ip·安全架构
Fuyo_11195 小时前
C++ 内存管理
c++·笔记
李伟_Li慢慢5 小时前
wolfram详解山峦算法
前端·算法
counting money5 小时前
prim算法最小生成树(java)
算法