C++ 竞赛入门

index

递归

本节学会什么

  • 判断一个问题能否拆成更小的同类问题
  • 找出递归边界和递归步骤
  • 用小数据跟踪递归调用过程

为什么需要它

有些问题的解决步骤和原问题相同,只是规模更小,例如枚举所有选择、遍历树或不断拆分区间。递归让函数调用自身来描述这种结构,但必须保证规模变小并最终停在边界。

最小可运行示例

#include <iostream>

void print_down(int n) {
    if (n == 0) {
        return;
    }
    std::cout << n << ' ';
    print_down(n - 1);
}

int main() {
    print_down(5);
    std::cout << '\n';
    return 0;
}

输出 5 4 3 2 1n == 0 是边界;其他情况先输出当前数,再把问题缩小为“从 n - 1 开始输出”。

再看一个把结果返回给上一层的例子:求 1n 的和。

#include <iostream>

int sum(int n) {
    if (n == 0) {
        return 0;
    }
    return n + sum(n - 1);
}

int main() {
    std::cout << sum(3) << '\n';
    return 0;
}

输出 6。调用 sum(3) 时先一路向下调用到边界 sum(0),再从最深处逐层向上计算。这里「更小的同类问题」的结果真正被使用了:sum(3) 的答案依赖 sum(2) 的答案,这就是目标一说的“把问题拆成更小的同类问题”。

flowchart TD
    s3["① sum(3): 求 3 + sum(2)"] --> s2["② sum(2): 求 2 + sum(1)"]
    s2 --> s1["③ sum(1): 求 1 + sum(0)"]
    s1 --> s0["④ sum(0): 到达边界,返回 0"]
    s0 --> r1["⑤ sum(1) 得到 0,返回 1"]
    r1 --> r2["⑥ sum(2) 得到 1,返回 3"]
    r2 --> r3["⑦ sum(3) 得到 3,返回 6"]

    classDef forward fill:#dcefe9,stroke:#087c6c,color:#202827
    classDef boundary fill:#fff4db,stroke:#b76e00,color:#202827
    classDef backtrack fill:#fbe3dd,stroke:#b9472f,color:#202827
    class s3,s2,s1 forward
    class s0 boundary
    class r1,r2,r3 backtrack

关键过程与易错点

  • 每个递归函数都要先写清边界条件,否则调用不会停止。
  • 每次递归调用都必须让问题更接近边界,例如 n - 1,而不是仍然传 n
  • 递归调用前的语句先执行,调用后的语句要等整层返回后再执行;把 print_down 的输出语句移到递归调用之后,输出就会变成升序。
  • 先用 n = 1、2、3 手工展开调用,再写代码;不要一开始就用大数据。
  • 递归有调用层数限制。深度很大时,循环或显式栈可能更合适。

动手检查

print_up(int n),使它输出 1n。提示:递归调用放在输出语句之前。对 n = 3 展开每一层,检查输出顺序。

print_down 的边界改错成 n < 0 后运行小数据,观察多输出了什么,再修正。

手算 sum(4) 的调用展开和每一层返回的值,再运行程序核对。

立即练习

难度 题目 题解 训练目标
基础 洛谷 P5727:冰雹猜想 题解 逆序输出整个变化序列,练习“先递归再输出”的调用顺序。
基础 洛谷 P1427:小鱼的数字游戏 题解 读入一串数后逆序输出;把输出语句放在递归调用之后。
基础 洛谷 P5743:猴子吃桃 题解 从最后一天往前倒推,用返回值递归写出“前一天 =(当天 + 1)× 2”。
巩固 洛谷 P1498:南蛮图腾 题解 先画出递归分解的层次,再实现同样的结构。