用"已知的结果"推出"下一个结果",一步一步向前——这就是递推。它是动态规划的前身,也是理解递归的基础。
递推的核心思想只有一句话:当前状态 = f(之前的状态)。
想象爬楼梯。你站在第 5 级台阶上,是因为你从第 4 级(或第 3 级)迈过来的。第 4 级是怎么到的?从第 3 级(或第 2 级)来的……每一级的答案,都建立在它之前的答案上。只要知道了最开始几级的情况,就能一路推算到任意一级。
递推的三个要素:
斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34 ……
规律:从第 3 项开始,每一项等于前两项之和。用数学式子写就是:
f(1) = 1,f(2) = 1,f(n) = f(n−1) + f(n−2) (n ≥ 3)
初始状态:f(1)=1,f(2)=1。递推关系:f(n) = f(n-1) + f(n-2)。三要素全齐,直接写代码:
| 1 | const int MAXN = 50; |
| 2 | long long f[MAXN]; |
| 3 | |
| 4 | int main() { |
| 5 | f[1] = 1; f[2] = 1; // ① 初始状态 |
| 6 | for (int i = 3; i <= 10; i++) // ③ 循环求解 |
| 7 | f[i] = f[i-1] + f[i-2]; // ② 递推关系式 |
| 8 | for (int i = 1; i <= 10; i++) |
| 9 | cout << f[i] << " "; // 1 1 2 3 5 8 13 21 34 55 |
| 10 | } |
循环从 i=3 开始,每次用已知的两个值推出新的一个,绿色格子是已知值,红色是当前正在求的值:
递推不止一种形式,下面是竞赛中常见的几类:
| 1 | // 到第 i 级 = 从第 i-1 级走一步 + 从第 i-2 级走两步 |
| 2 | // 本质和斐波那契完全一样,换了个描述 |
| 3 | long long dp[MAXN]; |
| 4 | dp[1] = 1; dp[2] = 2; // 初始:到第1级1种,到第2级2种 |
| 5 | for (int i = 3; i <= n; i++) |
| 6 | dp[i] = dp[i-1] + dp[i-2]; |
| 1 | // 三角形第 i 行第 j 列:可从上一行第 j-1 或第 j 列走来 |
| 2 | for (int i = 2; i <= n; i++) |
| 3 | for (int j = 1; j <= i; j++) |
| 4 | dp[i][j] = max(dp[i-1][j-1], dp[i-1][j]) + a[i][j]; |
| 5 | // 当前格 = max(左上格, 右上格) + 当前格的值 |
| 1 | // dp[i]:以 a[i] 结尾的最长不下降子序列长度 |
| 2 | for (int i = 1; i <= n; i++) { |
| 3 | dp[i] = 1; // 初始:至少只有自己这一个 |
| 4 | for (int j = 1; j < i; j++) |
| 5 | if (a[j] <= a[i]) // j 可以接在 i 前面 |
| 6 | dp[i] = max(dp[i], dp[j] + 1); |
| 7 | } |
无论哪种递推题,代码骨架几乎都是这样:
| 1 | // 一维递推 |
| 2 | dp[0] = 初始值; dp[1] = 初始值; // ① 边界 |
| 3 | for (int i = 2; i <= n; i++) // ③ 循环 |
| 4 | dp[i] = f(dp[i-1], dp[i-2], ...); // ② 关系式 |
| 5 | |
| 6 | // 二维递推(如网格/矩阵) |
| 7 | // 初始化第 0 行和第 0 列 |
| 8 | for (int i = 1; i <= n; i++) |
| 9 | for (int j = 1; j <= m; j++) |
| 10 | dp[i][j] = f(dp[i-1][j], dp[i][j-1], ...); |
递推和递归都能解决"用之前的结果推出当前结果"的问题,但方向相反:
| 对比项 | 递推(本节) | 递归(下一节) |
|---|---|---|
| 方向 | 从小到大,自底向上 | 从大到小,自顶向下 |
| 实现方式 | for 循环 + 数组 | 函数调用自身 |
| 优点 | 效率高,无函数调用开销,不会栈溢出 | 思路直接,代码与数学定义接近 |
| 缺点 | 需要想清楚填表顺序,二维时容易搞错方向 | 重复计算多(需加记忆化优化),深度大时栈溢出 |
| 适用场景 | 能明确推导顺序的问题,竞赛中首选 | 推导方向不固定、子问题结构复杂的问题 |