在做 DP 题时,最常遇到的困境是:"看题解秒懂,自己写就懵,尤其是那个核心的转移方程,到底是怎么想出来的?"
今天,从 LeetCode 上一道最经典的入门题------70. 爬楼梯 提炼出一套普适的动态规划解题框架。
一、 引言:从爬楼梯说起
题目描述:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
这题看似是排列组合,实际上是一道标准的动态规划题。
二、 动态规划的核心:状态与转移
动态规划的本质是通过定义状态和状态之间的递推关系,将大问题拆解为小问题,并利用历史结果避免重复计算。
要写出正确的代码,必须回答两个问题:
状态定义(State) :
dp[i]代表什么?状态转移方程(Transition) :
dp[i]和dp[i-1]、dp[i-2]等有什么关系?
1. 状态定义
对于爬楼梯,最自然的定义是:
dp[i]表示:到达第i阶楼梯的方法总数。
2. 推导状态转移方程(核心难点)
如何从已知状态推导出 dp[i]?
关键思考法:最后一步法(逆向思维)
想象你已经站在了第 i 阶楼梯上。你是怎么上来的?题目规定你只能爬 1 阶或 2 阶。
情况 A:你是从第
i-1阶爬 1 阶上来的。情况 B:你是从第
i-2阶爬 2 阶上来的。
因为这两条路径是互斥的(要么最后一步跨1阶,要么跨2阶),所以到达第 i 阶的方法总数 = 到达第 i-1 阶的方法数 + 到达第 i-2 阶的方法数。
由此得出转移方程:
dp[i]=dp[i−1]+dp[i−2]
注意:这里用的是加法,而不是 max,因为题目求的是"总数",不是"最大步数"。
3. 边界条件与初始化
dp[1] = 1:到第1阶只有1种方法(爬1阶)。
dp[2] = 2:到第2阶有2种方法(1+1 或 2)。
dp[0]:理论上不需要用到,但有时为了方便计算可以设为1。
4. 正确的代码实现(C++)(其实就是斐波那契数列)
cpp
class Solution {
public:
int climbStairs(int n) {
// 边界判断:防止数组越界
if (n <= 2) return n;
vector<int> dp(n + 1);
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
// 状态转移方程
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
};

进阶优化:由于 dp[i] 只依赖前两个状态,可以把空间复杂度从 O(N) 优化到 O(1),用两个变量滚动更新即可。
三、 普适化:如何识别不同类型的动态转移方程?
爬楼梯是"线性DP"的入门,但在实际算法题中,DP 题型千变万化。下面是 DP 常见的转移方程类型的五大模式。
模式一:线性递推(爬楼梯型)
-
特征:状态按照线性顺序(如数组下标、楼梯阶数)依次递推。
-
方程形式 :
dp[i] = dp[i-1] + dp[i-2]或带有权重的dp[i] = min(dp[i-1], dp[i-2]) + cost[i]。
模式二:区间 DP(合并型)
-
特征 :状态通常定义在区间
[i, j]上,大区间的解由小区间合并而来。 -
方程形式 :
dp[i][j] = min/max(dp[i][k] + dp[k+1][j] + cost),其中k是分割点。 -
思考技巧 :枚举分割点。把大问题切分成两个子问题,再加合起来的代价。
模式三:背包 DP(选择型)
-
特征:给定一个容量限制(背包),在若干物品中选择,求最大价值或方案数。
-
方程形式:
-
0-1背包(每个物品只能选一次):
dp[j] = max(dp[j], dp[j-w[i]] + v[i])(注意逆序遍历j)。 -
完全背包(物品无限):
dp[j] = max(dp[j], dp[j-w[i]] + v[i])(正序遍历j)。
-
-
思考技巧 :"选"与"不选" 。当前容量
j,对于物品i,要么不选(保持dp[j]),要么选(腾出空间w[i]放入)。
模式四:双序列 DP(匹配型)
-
特征:涉及两个字符串或两个数组,通常需要二维 DP 表。
-
方程形式 :
dp[i][j]表示A的前i个和B的前j个的某种关系。-
若
A[i] == B[j],则dp[i][j] = dp[i-1][j-1] + 1(或直接继承)。 -
若不等,则
dp[i][j] = max(dp[i-1][j], dp[i][j-1])。
-
-
思考技巧 :画二维表格。考虑当前字符匹配时,左上角怎么转移;不匹配时,左边或上边怎么转移。
模式五:树形 DP
-
特征:数据结构是树,状态在递归回溯时从子节点传递给父节点。
-
方程形式 :通常结合 DFS,
dp[node][0/1]表示节点选或不选的状态。 -
思考技巧 :后序遍历。先算清楚左右子树的状态,再决定当前节点的状态。
四、 总结:动态规划解题四步走
当你面对一道新题,怀疑它是 DP 时,请按以下步骤操作:
-
定义状态 :明确
dp数组的含义(一维还是二维?下标代表什么?)。 -
找转移方程 :思考**"最后一步"** 是怎么来的,或者**"当前状态"** 可以由哪些**"前驱状态"**推导出来。这是最关键的一步,通常需要分类讨论(如选/不选、匹配/不匹配)。
-
确定初始值 :
dp[0]、dp[1]等边界是什么?注意数组越界问题(如爬楼梯中n=1的情况)。 -
确定遍历顺序 :是从前往后,还是从后往前?是外层循环物品还是内层循环容量?确保计算
dp[i]时,依赖的状态已经计算完毕。