Skip to content

第6章 递推算法 ​


🎯 本章学习目标 ​

通过本章的学习,你将能够:

  1. 理解递推思想:掌握递推的核心——从已知出发,逐步推算未知
  2. 掌握递推三要素:能找出初始条件、递推关系式和终止条件
  3. 实现一维递推:能用变量迭代或数组实现斐波那契等一维递推
  4. 解决经典问题:掌握爬楼梯、汉诺塔、兔子繁殖等经典递推模型
  5. 实现二维递推:能用二维数组解决路径计数、数字三角形等问题
  6. 处理边界条件:正确处理首行首列、障碍格等边界情况
  7. 区分递推与递归:理解二者的区别和联系
  8. 防止数据溢出:知道什么时候该用 long long 类型
  9. 选择实现方式:根据问题特点选择变量迭代法或数组存储法
  10. 编写递推代码:独立完成过河卒、数字三角形等编程题

🗺️ 本章知识图谱 ​

递推算法
├── 基础概念
│   ├── 递推的核心思想
│   ├── 递推三要素
│   │   ├── 初始条件(起点)
│   │   ├── 递推关系式(怎么推)
│   │   └── 终止条件(到哪停)
│   └── 递推 vs 递归
├── 一维递推
│   ├── 斐波那契数列
│   ├── 爬楼梯问题
│   ├── 汉诺塔问题
│   ├── 兔子繁殖问题
│   └── 铺砖问题
├── 二维递推
│   ├── 路径计数(无障碍)
│   ├── 过河卒(含障碍)
│   ├── 数字三角形(最大路径和)
│   └── 杨辉三角
├── 实现方式
│   ├── 变量迭代法(O(1)空间)
│   └── 数组存储法(O(n)/O(n²)空间)
├── 边界处理
│   ├── 初始条件(多个初值)
│   ├── 首行/首列特殊处理
│   ├── 障碍格(dp=0)
│   └── 数组越界防范
└── 注意事项
    ├── 数据类型溢出(用long long)
    ├── 下标一致性(0开始vs1开始)
    └── 递推方向(顺序不能错)

一、什么是递推? ​

递推(Recurrence)的核心思想:从已知的初始条件出发,通过递推关系式逐步演算,最终推导出目标结果。

通俗地说,就是"知道前几个,算下一个"。就像多米诺骨牌——推倒第一块(初始条件),后面的每一块都被前一块推倒(递推关系),直到最后一块倒下(终止条件)。

💡 生活中的递推:你第一天背了10个单词,以后每天比前一天多背2个,第n天背多少个?答案:10 + 2×(n-1)。这就是最简单的递推——首项已知,公差确定,逐项推算。


二、递推三要素 ​

做任何递推题,先问自己三个问题:

要素含义关键问题
① 初始条件递推的起点,已知的最小情况的值"最早能确定的是哪些值?"
② 递推关系式如何从前面的状态推出当前状态"f(n) 与 f(n-1)、f(n-2) 等是什么关系?"
③ 终止条件递推到何时停止"算到哪个值就得到答案了?"

易错点 ⚠️: 三要素缺一不可!初始条件设错,后面全盘皆错;递推关系式推导有误,结果必然不对;终止条件搞错,可能多算少算或死循环。

递推关系式的推导方法 ​

递推关系式不会凭空出现,从特例入手找规律是最实用的推导方法:

  1. 手动模拟:先算出 n=1, 2, 3, 4 时的结果
  2. 观察规律:对比相邻项,猜测 f(n) 与前面哪些项有关
  3. 验证猜想:用更多特例验证猜测是否成立
  4. 写出公式:确认规律后写出递推关系式

🔥 高频考点:考试中递推题往往不直接给出公式,需要你自己从问题描述中推导。务必先手动算前几项找规律,不要急着写代码!


三、递推与递归的区别 ​

递推和递归都涉及"自己与自己的关系",但实现思路截然不同:

对比维度递推(循环实现)递归(函数自调用)
思考方向自底向上(bottom-up),从已知推未知自顶向下(top-down),把大问题拆成小问题
实现方式循环结构,逐步迭代函数调用自身
时间效率无函数调用开销,更快有调用开销,可能大量重复计算
空间效率可做到 O(1)需要栈空间,通常 O(n)
栈溢出风险无有(层数过深时)
代码可读性直观,按部就班简洁但对初学者不直观
cpp
#include <iostream>
using namespace std;

// 递推写法:从小到大,逐步迭代
long long fib_iter(int n) {
    if (n <= 2) return 1;
    long long a = 1, b = 1;       // a=F(n-2), b=F(n-1)
    for (int i = 3; i <= n; i++) {
        long long c = a + b;      // F(n) = F(n-2) + F(n-1)
        a = b;
        b = c;
    }
    return b;
}

// 递归写法:自顶向下,层层分解
long long fib_recur(int n) {
    if (n <= 2) return 1;         // 递归终止条件
    return fib_recur(n - 1) + fib_recur(n - 2);
}

int main() {
    cout << fib_iter(40) << endl;   // 瞬间完成
    // cout << fib_recur(40) << endl; // 明显变慢,大量重复计算
    return 0;
}

易错点 ⚠️: 递归求斐波那契存在指数级重复计算——fib(3) 被算2次,fib(2) 被算3次,n=40 时递归已明显卡顿,而递推瞬间完成。考试中只要求结果时,优先用递推。


四、经典一维递推问题 ​

4.1 斐波那契数列(Fibonacci) ​

问题描述: 斐波那契数列定义如下:

  • F(1) = 1, F(2) = 1
  • F(n) = F(n-1) + F(n-2)(n ≥ 3)

前几项为:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ...

递推三要素:

  • 初始条件:F(1) = 1, F(2) = 1
  • 递推关系式:F(n) = F(n-1) + F(n-2)
  • 终止条件:计算到 F(n) 时停止
cpp
#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;

    long long f[105];
    f[1] = 1;                    // 初始条件
    f[2] = 1;                    // 初始条件
    for (int i = 3; i <= n; i++) {
        f[i] = f[i - 1] + f[i - 2];  // 递推关系式
    }
    cout << f[n] << endl;        // 终止条件:输出第n项

    return 0;
}
// 输入: 10
// 输出: 55

🔥 高频考点:斐波那契数列增长极快!F(46) ≈ 18亿,F(47) 就超过 int 范围(约21亿)。考试中 n 稍大就要用 long long。


4.2 爬楼梯问题 ​

问题描述: 一段楼梯有 n 级台阶,每次可以走1级或2级,走到第 n 级共有多少种不同的走法?

推导过程(从特例入手):

n走法枚举走法数 f(n)
1走1级1
21+1,直接走2级2
31+1+1,1+2,2+13
41+1+1+1,1+1+2,1+2+1,2+1+1,2+25

观察:f(3)=3 = f(2)+f(1),f(4)=5 = f(3)+f(2)。

规律: 到达第 n 级的最后一步只有两种可能:

  • 从第 n-1 级走 1级 上来 → f(n-1) 种
  • 从第 n-2 级走 2级 上来 → f(n-2) 种

因此 f(n) = f(n-1) + f(n-2)。

易错点 ⚠️: 公式和斐波那契一样,但初始条件不同!f(2) = 2 而不是 1。这是最容易照搬公式出错的地方——务必根据题意重新确定初始条件。

cpp
#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;

    long long f[105];
    f[1] = 1;                    // 到第1级:1种走法
    f[2] = 2;                    // 到第2级:2种走法(1+1 或 2)
    for (int i = 3; i <= n; i++) {
        f[i] = f[i - 1] + f[i - 2];
    }
    cout << f[n] << endl;

    return 0;
}
// 输入: 5
// 输出: 8

4.3 汉诺塔递推(移动次数) ​

问题描述: 有三根柱子 A、B、C,A 柱上套着 n 个从小到大的圆盘。每次只能移动一个圆盘,且大盘不能放在小盘上面。求把 n 个圆盘从 A 柱移到 C 柱最少需要移动多少次?

推导过程(从特例入手):

n移动策略次数 H(n)
1直接把圆盘从 A 移到 C1
2小盘A→B,大盘A→C,小盘B→C3
3先把2个盘A→B(3次),最大盘A→C(1次),再把2个盘B→C(3次)7

规律: 要移动 n 个圆盘:

  1. 把上面 n-1 个圆盘从 A 移到 B(借助 C)→ H(n-1) 次
  2. 把最大的圆盘从 A 移到 C → 1 次
  3. 把 n-1 个圆盘从 B 移到 C(借助 A)→ H(n-1) 次

因此 H(n) = 2 × H(n-1) + 1,H(1) = 1。

进一步可得通项公式:H(n) = 2ⁿ - 1。

cpp
#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;

    long long h[105];
    h[1] = 1;                         // 1个圆盘:移动1次
    for (int i = 2; i <= n; i++) {
        h[i] = 2 * h[i - 1] + 1;      // 递推:H(n) = 2*H(n-1) + 1
    }
    cout << h[n] << endl;

    return 0;
}
// 输入: 4
// 输出: 15
// 验证: H(4) = 2*7 + 1 = 15 = 2^4 - 1

🔥 高频考点:汉诺塔在四级中常考移动次数的递推计算(而非模拟移动过程)。公式 H(n) = 2ⁿ - 1 必须牢记,考试可直接用于验证答案。


4.4 铺砖问题 ​

问题描述: 用 1×2 的多米诺骨牌铺满一个 2×n 的矩形,有多少种不同的铺法?

推导过程(从特例入手):

n铺法示意铺法数 f(n)
1竖放1块1
2两块竖放 或 两块横放2
3竖放+2×2铺法 或 横放+2×1铺法3

规律: 考虑最右侧的铺法:

  • 竖放1块:占 2×1 空间,剩下 2×(n-1) → f(n-1) 种
  • 横放2块:上下各横放一块,占 2×2 空间,剩下 2×(n-2) → f(n-2) 种

因此 f(n) = f(n-1) + f(n-2),f(1) = 1, f(2) = 2。

💡 这个公式和爬楼梯完全一样,但问题场景不同。关键是学会从边界情况切入,分析"最后一步"的两种可能。

cpp
#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;

    long long f[105];
    f[1] = 1;                    // 2×1:1种(竖放)
    f[2] = 2;                    // 2×2:2种(两竖或两横)
    for (int i = 3; i <= n; i++) {
        // 最后竖放1块 + 最后横放2块
        f[i] = f[i - 1] + f[i - 2];
    }
    cout << f[n] << endl;

    return 0;
}
// 输入: 5
// 输出: 8

4.5 兔子繁殖问题 ​

问题描述: 有一对刚出生的小兔子,小兔子出生后第3个月开始每个月生一对小兔子。假如兔子不死,问第 n 个月共有多少对兔子?

推导过程(逐月模拟):

月份 n幼兔(当月出生)成兔(可繁殖)总数 f(n)
11对0对1
20对1对1
31对(成兔生的)1对2
41对2对3
52对3对5

规律分析:

  • 第 n 个月的成兔数 = 第 n-1 个月的总数(所有兔子都会长大)
  • 第 n 个月新生的幼兔数 = f(n-2)(成兔是两个月前出生的)
  • 因此:f(n) = 上月总数 f(n-1) + 新生幼兔 f(n-2)

即 f(n) = f(n-1) + f(n-2),f(1) = 1, f(2) = 1。

💡 这就是斐波那契数列的起源问题——斐波那契在《算盘全书》中以兔子繁殖为例子引入了这个数列。虽然公式相同,但推导过程值得反复体会。

cpp
#include <iostream>
using namespace std;

int main() {
    int n;
    cin >> n;

    long long f[105];
    f[1] = 1;                    // 第1月:1对幼兔
    f[2] = 1;                    // 第2月:1对成兔(还不能生)
    for (int i = 3; i <= n; i++) {
        // 当月总数 = 上月总数 + 当月新生(两月前的兔子数 = 可繁殖的成兔数)
        // 当月新生幼兔 = 可繁殖成兔数 = 两个月前的兔子总数
        //(第i-2月的所有兔子到第i月都长大成熟了)
        f[i] = f[i - 1] + f[i - 2];
    }
    cout << f[n] << endl;

    return 0;
}
// 输入: 10
// 输出: 55

五、经典二维递推问题 ​

当问题涉及二维网格或表格时,递推关系从"依赖前面的项"变为"依赖上方和左方的格",这就是二维递推。

5.1 过河卒问题 ​

问题描述: 棋盘上 A 点有一个过河卒,需要走到目标 B 点。卒只能向下或向右走。棋盘上有一个马(位于 (mx, my)),马的控制点(马走"日"字能到达的格子)不能走。求从起点 (0,0) 到终点 (n,m) 的路径条数。

推导过程:

设 dp[i][j] 表示从起点 (0,0) 走到 (i,j) 的路径条数。

  • 初始条件:dp[0][0] = 1(起点只有1种方式);第一行和第一列的格子都只有1种走法(一路走到底),但若遇到马的控制点则路径数为0
  • 递推关系式:dp[i][j] = dp[i-1][j] + dp[i][j-1](只能从上面或左边走来)
  • 终止条件:计算到 dp[n][m] 时停止

马的控制点有8个方向:(±1,±2) 和 (±2,±1)。

cpp
#include <iostream>
using namespace std;

int main() {
    int n, m, mx, my;
    cin >> n >> m >> mx >> my;  // 终点(n,m),马的位置(mx,my)

    long long dp[25][25] = {0};
    bool blocked[25][25] = {false};

    // 标记马的位置和8个控制点为障碍
    int dx[] = {0, 1, 1, -1, -1, 2, 2, -2, -2};
    int dy[] = {0, 2, -2, 2, -2, 1, -1, 1, -1};
    for (int k = 0; k < 9; k++) {
        int nx = mx + dx[k];
        int ny = my + dy[k];
        if (nx >= 0 && nx <= n && ny >= 0 && ny <= m) {
            blocked[nx][ny] = true;
        }
    }

    // 递推
    for (int i = 0; i <= n; i++) {
        for (int j = 0; j <= m; j++) {
            if (blocked[i][j]) {
                dp[i][j] = 0;            // 障碍格:路径数为0
                continue;
            }
            if (i == 0 && j == 0) {
                dp[0][0] = 1;            // 起点
            } else if (i == 0) {
                dp[i][j] = dp[i][j - 1]; // 第一行:只能从左边来
            } else if (j == 0) {
                dp[i][j] = dp[i - 1][j]; // 第一列:只能从上面来
            } else {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; // 上面+左边
            }
        }
    }

    cout << dp[n][m] << endl;

    return 0;
}
// 输入: 6 6 3 2
// 输出: 17

🔥 高频考点:过河卒是二维递推的经典考题,几乎每年都会以不同形式出现。关键要点:① 障碍格令 dp[i][j]=0;② 边界(第一行/列)的路径数需单独递推,不能直接填1;③ 使用 long long 防止路径数溢出。


5.2 数字三角形 ​

问题描述: 给定一个数字三角形,从顶部出发,每次可以走到左下方或右下方的相邻数字,求从顶到底的路径上数字之和的最大值。

       7
      3 8
     8 1 0
    2 7 4 4
   4 5 2 6 5

推导过程:

设 dp[i][j] 表示从顶部走到第 i 行第 j 列时的最大路径和。

  • 初始条件:dp[0][0] = a[0][0](顶点数字)
  • 递推关系式:
    • 左端(j=0):只能从正上方走来 → dp[i][0] = dp[i-1][0] + a[i][0]
    • 右端(j=i):只能从左上方走来 → dp[i][i] = dp[i-1][i-1] + a[i][i]
    • 中间:取左上方和正上方的较大值 → dp[i][j] = max(dp[i-1][j-1], dp[i-1][j]) + a[i][j]
  • 终止条件:最后一行的最大值 max(dp[n-1][j])
cpp
#include <iostream>
#include <algorithm>
using namespace std;

int main() {
    int n;
    cin >> n;

    int a[105][105] = {0};
    long long dp[105][105] = {0};

    // 读入数字三角形
    for (int i = 0; i < n; i++) {
        for (int j = 0; j <= i; j++) {
            cin >> a[i][j];
        }
    }

    dp[0][0] = a[0][0];              // 初始条件:顶点

    for (int i = 1; i < n; i++) {
        for (int j = 0; j <= i; j++) {
            if (j == 0) {
                // 左端:只能从正上方来
                dp[i][j] = dp[i - 1][j] + a[i][j];
            } else if (j == i) {
                // 右端:只能从左上方来
                dp[i][j] = dp[i - 1][j - 1] + a[i][j];
            } else {
                // 中间:取上方两个方向的较大值
                dp[i][j] = max(dp[i - 1][j - 1], dp[i - 1][j]) + a[i][j];
            }
        }
    }

    // 答案为最后一行的最大值
    long long ans = 0;
    for (int j = 0; j < n; j++) {
        ans = max(ans, dp[n - 1][j]);
    }
    cout << ans << endl;

    return 0;
}
// 输入:
// 5
// 7
// 3 8
// 8 1 0
// 2 7 4 4
// 4 5 2 6 5
// 输出: 30
// 最优路径: 7→3→8→7→5 = 30

易错点 ⚠️: 数字三角形的边界处理是关键——左端和右端的递推来源不同,不能统一用 max。考试中如果忽略边界,会导致数组下标越界或取到错误的0值。


六、递推的边界处理 ​

边界处理是递推题中最容易丢分的环节,以下是常见边界问题及应对策略:

边界问题典型场景处理方法
初始条件多解斐波那契需 f(1) 和 f(2) 两个初值逐个列出所有初始条件,不要遗漏
首行/首列特殊过河卒第一行只能从左来单独循环初始化,或在主循环中判断
数组越界dp[i][j] 访问 dp[i-1][j] 时 i=0从 i=1 开始递推,i=0 单独处理
障碍格过河卒马的控制点令障碍格 dp=0,且不参与递推传播
数据溢出斐波那契、路径计数统一使用 long long
下标从0还是1杨辉三角从0开始,斐波那契从1开始根据题目习惯选择,保持一致

易错点 ⚠️: 过河卒中第一行/第一列的格子,如果前面有障碍格,后面的格子路径数也应该是0(无法绕过障碍到达)。因此边界不能简单填1,而要逐格递推——这是最常见的丢分点!


七、递推的两种代码实现方式 ​

方式空间复杂度适用场景典型问题
变量迭代法O(1)只需最终结果,递推只依赖前1~2项斐波那契、爬楼梯、汉诺塔
数组存储法O(n) 或 O(n²)需保存中间结果,或依赖多个前驱状态杨辉三角、过河卒、数字三角形

选择原则:

  • 只需要 f(n) 且公式只依赖前1~2项 → 变量迭代法,节省空间
  • 需要输出所有中间结果,或二维递推 → 数组存储法
  • 考场上拿不准时 → 优先用数组存储法,逻辑清晰、不易出错

八、📝 实战练习 ​

练习题 1(单选题) ​

题目: 已知数列定义如下:a₁ = 1, a₂ = 2, a₃ = 3,且对于 n ≥ 4,有 aₙ = aₙ₋₁ + aₙ₋₂ + aₙ₋₃。求 a₈ 的值。 A. 37  B. 48  C. 61  D. 68

🔑 点击查看答案与解析

答案: D

解析: 逐项递推计算:

n计算过程aₙ
1初始值1
2初始值2
3初始值3
4a₃+a₂+a₁ = 3+2+16
5a₄+a₃+a₂ = 6+3+211
6a₅+a₄+a₃ = 11+6+320
7a₆+a₅+a₄ = 20+11+637
8a₇+a₆+a₅ = 37+20+1168

a₈ = 68,选 D。

易错点 ⚠️: 三阶递推(依赖前三项)需要三个初始值。考试中不要看到类似斐波那契的形式就只设两个初值,务必数清楚递推公式依赖几项!


练习题 2(单选题) ​

题目: 将 n 个圆盘从 A 柱移到 C 柱(借助 B 柱),每次只能移动一个圆盘且大盘不能放在小盘上面。若移动一个圆盘需要1秒钟,H(n) 表示移动 n 个圆盘所需的最少次数,则 H(5) 的值及 H(n) 的通项公式分别为? A. 15, H(n) = 2ⁿ - 1  B. 31, H(n) = 2ⁿ - 1
C. 31, H(n) = 2n - 1  D. 63, H(n) = 2ⁿ - 1

🔑 点击查看答案与解析

答案: B

解析:

递推关系式:H(n) = 2 × H(n-1) + 1,H(1) = 1。

逐项计算:

n计算过程H(n)
1初始值1
22×1+13
32×3+17
42×7+115
52×15+131

通项公式推导:H(n) = 2ⁿ - 1(可用数学归纳法证明)。

H(5) = 2⁵ - 1 = 31,选 B。

🔥 考试技巧:汉诺塔题的关键是理解"先移 n-1 个盘到辅助柱,再移最大盘,最后移 n-1 个盘到目标柱"的三步策略,由此自然得到递推公式 H(n)=2H(n-1)+1。通项 2ⁿ-1 建议直接记住,考试中可用于快速验证。


练习题 3(填空题) ​

题目: 在一个 4×4 的网格中(从 (0,0) 到 (4,4)),每次只能向右或向下走一步,但 (2,2) 处有障碍不能经过。问共有多少条不同的路径?

🔑 点击查看答案与解析

答案: 34

解析:

使用二维递推,dp[i][j] 表示从 (0,0) 到 (i,j) 的路径数。障碍格 (2,2) 令 dp[2][2] = 0。

填充 dp 表(第一行和第一列均为1,障碍处为0):

  1  1  1  1  1
  1  2  3  4  5
  1  3  0  4  9
  1  4  4  8 17
  1  5  9 17 34

递推过程(以关键格为例):

  • dp[2][2] = 0(障碍)
  • dp[2][3] = dp[1][3] + dp[2][2] = 4 + 0 = 4
  • dp[3][2] = dp[2][2] + dp[3][1] = 0 + 4 = 4
  • dp[3][3] = dp[2][3] + dp[3][2] = 4 + 4 = 8
  • dp[4][4] = dp[3][4] + dp[4][3] = 17 + 17 = 34

验证(容斥原理):

  • 无障碍总路径数 = C(8,4) = 70
  • 经过 (2,2) 的路径数 = C(4,2) × C(4,2) = 6 × 6 = 36
  • 不经过 (2,2) 的路径数 = 70 - 36 = 34 ✓
cpp
#include <iostream>
using namespace std;

int main() {
    int n = 4;
    long long dp[10][10] = {0};
    bool blocked[10][10] = {false};
    blocked[2][2] = true;  // 障碍格

    for (int i = 0; i <= n; i++) {
        for (int j = 0; j <= n; j++) {
            if (blocked[i][j]) {
                dp[i][j] = 0;
                continue;
            }
            if (i == 0 && j == 0) dp[0][0] = 1;
            else if (i == 0) dp[i][j] = dp[i][j - 1];
            else if (j == 0) dp[i][j] = dp[i - 1][j];
            else dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
        }
    }

    cout << dp[n][n] << endl;  // 输出: 34
    return 0;
}

🔥 高频考点:含障碍的路径计数是过河卒问题的简化版,核心思路完全一致——障碍格 dp=0,其余格 dp[i][j]=dp[i-1][j]+dp[i][j-1]。无障碍时可用组合数 C(m+n, m) 快速验证。


练习题 4(单选题) ​

题目: 小明爬楼梯,每次可以走 1 级或 2 级。问爬 10 级楼梯共有多少种不同的走法?( ) A. 55
B. 89
C. 144
D. 233

🔑 点击查看答案与解析

答案: B

解析:

这是经典的爬楼梯问题,递推关系式:f(n) = f(n-1) + f(n-2)

  • 到第 n 级的最后一步,要么从 n-1 走 1 级上来,要么从 n-2 走 2 级上来
  • 初始条件:f(1) = 1(只有1种走法),f(2) = 2(1+1 或 2)

逐项计算:

  • f(1) = 1
  • f(2) = 2
  • f(3) = f(2) + f(1) = 2 + 1 = 3
  • f(4) = 3 + 2 = 5
  • f(5) = 5 + 3 = 8
  • f(6) = 8 + 5 = 13
  • f(7) = 13 + 8 = 21
  • f(8) = 21 + 13 = 34
  • f(9) = 34 + 21 = 55
  • f(10) = 55 + 34 = 89

选 B。

💡 注意:爬楼梯数列和斐波那契数列类似,但起始项不同。f(1)=1, f(2)=2 对应斐波那契的 f(2)=1, f(3)=2 错开了一位。


练习题 5(判断题) ​

题目: 递推算法必须用递归函数来实现。( )

🔑 点击查看答案与解析

答案: 错误 ❌

解析: 递推是一种算法思想,递归是一种代码实现方式。递推可以用循环(迭代)实现,也可以用递归实现。实际上,递推用循环实现效率更高(避免递归调用的开销和栈溢出风险)。

🍎 生活比喻:递推是"从一楼往上爬楼梯"(从已知往目标推),递归是"从十楼往下问"(把大问题拆成小问题)。两种方式都能到达目的地,但方向相反。


练习题 6(填空题) ​

题目: 数字三角形递推公式:dp[i][j] = max(dp[i-1][j-1], dp[i-1][j]) + a[i][j],其中左端 j=0 和右端 j=i 需要特殊处理。

左端的递推式应为:dp[i][0] = ________

右端的递推式应为:dp[i][i] = ________

🔑 点击查看答案与解析

答案:

  • 左端:dp[i-1][0] + a[i][0](只能从正上方走来)
  • 右端:dp[i-1][i-1] + a[i][i](只能从左上方走来)

解析:

数字三角形第 i 行第 j 列:

  • 左端(j=0):只能从正上方(i-1, 0)走下来,没有左上方
  • 右端(j=i):只能从左上方(i-1, i-1)走下来,没有正上方
  • 中间:可以从左上方或正上方走下来,取较大值

⚠️ 易错点:如果忽略边界直接写 max(dp[i-1][j-1], dp[i-1][j]),当 j=0 时会访问 dp[i-1][-1] 导致越界!边界必须单独处理。


练习题 7(填空题) ​

题目: 假设一对刚出生的小兔,一个月后长成大兔,再过一个月就能生下一对小兔,并且此后每个月都生一对小兔。如果没有死亡,问第 6 个月共有多少对兔子?

答案: ______ 对

🔑 点击查看答案与解析

答案: 8

解析:

这是经典的兔子繁殖问题,就是斐波那契数列。

设 f(n) 为第 n 个月的兔子对数:

  • 第 1 个月:1 对(刚出生的小兔)→ f(1) = 1
  • 第 2 个月:1 对(长成大兔,还不能生)→ f(2) = 1
  • 第 3 个月:原来的1对大兔 + 新生的1对小兔 = 2对 → f(3) = f(2) + f(1) = 2
  • 第 4 个月:f(4) = f(3) + f(2) = 2 + 1 = 3
  • 第 5 个月:f(5) = 3 + 2 = 5
  • 第 6 个月:f(6) = 5 + 3 = 8

递推公式:f(n) = f(n-1) + f(n-2),本质就是斐波那契数列。

💡 记忆要点:当月兔子数 = 上月兔子数 + 上上月兔子数(上上月的兔子这个月都能生小兔子了)。


练习题 8(编程题) ​

题目: 猴子吃桃问题:猴子第一天摘了若干个桃子,每天吃一半多一个,到第 7 天早上想再吃时,只剩下 1 个桃子了。求第一天共摘了多少个桃子?

提示:用倒推法——从第 7 天的 1 个桃子,反推第 6 天、第 5 天...直到第 1 天。

🔑 点击查看答案与解析

参考答案:

cpp
#include <iostream>
using namespace std;

int main() {
    int peach = 1;  // 第7天早上剩下1个
    // 倒推:从第6天推到第1天,共推6次
    for (int day = 6; day >= 1; day--) {
        // 当天桃子数 = (前一天桃子数 + 1) × 2
        // 因为:前一天吃了一半多一个,剩下的是一半少一个
        // 所以:剩下的 = 当天/2 - 1 → 当天 = (剩下的 + 1) * 2
        peach = (peach + 1) * 2;
    }
    cout << peach << endl;
    return 0;
}
// 输出:190

解析:

这是一道经典的倒推递推题。正向计算不知道初始值,但知道最终结果和变化规律,可以倒着推。

正推公式:当天剩下 = 前一天总数 / 2 - 1 倒推公式:前一天总数 = (当天剩下 + 1) × 2

从第 7 天的 1 个开始倒推 6 次:

  • 第 6 天:(1 + 1) × 2 = 4
  • 第 5 天:(4 + 1) × 2 = 10
  • 第 4 天:(10 + 1) × 2 = 22
  • 第 3 天:(22 + 1) × 2 = 46
  • 第 2 天:(46 + 1) × 2 = 94
  • 第 1 天:(94 + 1) × 2 = 190

💡 递推技巧:正向不好算就试试倒推!知道终点和变化规则,反向推导也是递推的常用思路。


九、📊 考点总结表 ​

考点重要程度出题形式易错点
递推三要素⭐⭐⭐⭐选择/填空/编程遗漏初始条件或递推式写错
斐波那契及变体⭐⭐⭐⭐⭐选择/程序阅读/编程初始条件记错,递推项数搞错
爬楼梯问题⭐⭐⭐⭐选择/编程起始项和斐波那契错开一位
汉诺塔 H(n)=2ⁿ-1⭐⭐⭐⭐选择/判断通项公式记混,n=1时验证
过河卒(含障碍)⭐⭐⭐⭐⭐编程题边界有障碍时不能直接填1,要逐格递推
数字三角形⭐⭐⭐⭐编程题左右两端边界处理错误导致越界
边界处理⭐⭐⭐⭐⭐贯穿所有编程题首行首列、障碍格考虑不周
long long 防溢出⭐⭐⭐⭐贯穿所有题路径数、斐波那契数容易溢出int
递推 vs 递归⭐⭐⭐选择/判断递推是思想,递归是实现方式

⭐ 必背速记卡 ​

┌─────────────────────────────────────────────┐
│          递推算法 · 必背速记卡               │
├─────────────────────────────────────────────┤
│  1. 递推三要素:初始条件+递推式+终止条件     │
│  2. 斐波那契:f(n)=f(n-1)+f(n-2), f(1)=1,f(2)=1│
│  3. 爬楼梯:f(n)=f(n-1)+f(n-2), f(1)=1,f(2)=2│
│  4. 汉诺塔:H(n)=2ⁿ-1,递推式 H(n)=2H(n-1)+1 │
│  5. 兔子繁殖 = 斐波那契数列                  │
│  6. 路径计数:dp[i][j]=dp[i-1][j]+dp[i][j-1]│
│  7. 过河卒:障碍格dp=0,阻断后续路径         │
│  8. 过河卒边界有障碍时,后面的格子路径数为0  │
│  9. 数字三角形:dp[i][j]=max(左上,正上)+a[i][j]│
│ 10. 数字三角形两端:只能从一个方向来         │
│ 11. 递推实现:变量迭代法 O(1),数组法 O(n)   │
│ 12. 优先用数组法:逻辑清晰不易错              │
│ 13. 路径数、斐波那契数用 long long 防溢出    │
│ 14. 递推方向不能反:必须保证算当前时前面已算出│
│ 15. 正向不行就倒推,知道终点也能递推         │
└─────────────────────────────────────────────┘

百炼成钢,融会贯通