动态规划
动态规划 (Dynamic Programming, DP)
像“带着备忘录爬山”一样,把问题拆成有重叠的子问题。为了避免重复计算,把已经算过的子问题结果记在“表格”里。下次再遇到同样的子问题,直接查表,用空间换时间。
经典问题推演:斐波那契数列
状态定义:dp[i] 表示第 i 个斐波那契数。
边界条件:dp[0] = 0, dp[1] = 1。
状态转移方程:dp[i] = dp[i-1] + dp[i-2]。
填表过程:从 i=2 开始,利用前面已经算好的 dp[1] 和 dp[0] 算出 dp[2],再算 dp[3]... 一路推到 dp[n]。
经典问题推演:爬楼梯问题 (Climbing Stairs)
问题描述:假设你正在爬楼梯,需要 n 阶你才能到达楼顶。每次你可以选择爬 1 个台阶或者 2 个台阶。请问你有多少种不同的方法可以爬到楼顶?
示例:输入 n = 3,输出 3(因为方法有:1+1+1,1+2,2+1)。
为什么用动态规划?
如果用暴力递归,你会发现计算 f(5) 时需要计算 f(4) 和 f(3),而计算 f(4) 时又要算一次 f(3)。大量的子问题被反复计算,导致时间复杂度呈指数级爆炸。而动态规划通过“记住”已经算过的结果,完美解决了这个问题。
核心思想:三大核心要素
最优子结构:当前问题的解,可以由子问题的解组合得到。
重叠子问题:在递归展开时,会反复遇到相同规模的子问题。
状态转移方程:找到当前状态与前序状态之间的数学关系。
核心代码
#include <stdio.h>
// 动态规划求解爬楼梯
int climbStairs(int n) {
// 1. 边界条件:处理最基础的子问题
if (n <= 2) return n;
// 2. 定义状态表(这里为了节省空间,用三个变量代替数组)
int prev2 = 1; // f(1) = 1
int prev1 = 2; // f(2) = 2
int current;
// 3. 状态转移方程:自底向上填表
for (int i = 3; i <= n; i++) {
current = prev1 + prev2; // f(i) = f(i-1) + f(i-2)
prev2 = prev1; // 状态滚动更新
prev1 = current;
}
// 4. 返回最终结果
return prev1;
}
int main() {
int n = 5;
printf("爬到 %d 阶楼梯的方法数为:%d\n", n, climbStairs(n));
return 0;
}
示例推演
以 n = 5 阶楼梯为例,我们来看看自底向上的“填表”过程:
第 1 步:确定边界条件(初始化)
目标:找到不需要推导就能直接得出答案的基础情况。
爬 1 阶:只有 1 种方法(迈 1 步)。所以 f(1) = 1
爬 2 阶:有 2 种方法(1+1,或直接迈 2 步)。所以 f(2) = 2
第 2 步:推导状态转移方程
目标:找出 f(n) 和前面状态的关系。
思考:要到达第 n 阶,你的最后一步只有两种可能:
从第 n-1 阶,跨 1 步上来。
从第 n-2 阶,跨 2 步上来。
结论:到达第 n 阶的方法数 = 到达 n-1 阶的方法数 + 到达 n-2 阶的方法数。
状态转移方程:f(n) = f(n-1) + f(n-2)
第 3 步:自底向上填表(迭代计算)
目标:利用已知推导未知,避免重复计算。
计算 f(3):f(2) + f(1) = 2 + 1 = 3
计算 f(4):f(3) + f(2) = 3 + 2 = 5
计算 f(5):f(4) + f(3) = 5 + 3 = 8
最终结果
爬到 5 阶楼梯共有 8 种不同的方法。
四步法:
定义状态:明确 dp 数组或变量的含义(如 f(n) 表示爬到第 n 阶的方法数)。
确定转移方程:找出状态之间的关系式(如 f(n) = f(n-1) + f(n-2))。
设置初始条件:给出最小子问题的解(如 f(1)=1, f(2)=2)。
选择计算顺序:通常采用自底向上(迭代填表)的方式。
空间优化技巧:当状态转移仅依赖于前几个状态时,可以使用滚动变量(如代码中的 prev1, prev2)代替一维数组,将空间复杂度从 \(O(n)\) 降至 \(O(1)\) 。
核心提炼
两大核心性质:必须同时具备最优子结构(大问题的最优解包含子问题的最优解)和重叠子问题(子问题会被反复计算)。
与分治的区别:分治的子问题独立,DP的子问题重叠;DP需要额外的表格空间。

浙公网安备 33010602011771号