1. 动态规划入门三部曲:从斐波那契到最小花费爬楼梯
最近在算法训练中重新认识了动态规划(DP)这个老朋友。很多人觉得DP高深莫测,但通过这三道经典题目——斐波那契数、爬楼梯和最小花费爬楼梯,我发现DP的核心思想其实非常直观。下面就用工程师的视角,拆解这三道题背后的通用解题框架和实现细节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. DP五步法实战解析
2.1 斐波那契数列:DP的Hello World
斐波那契数列(LeetCode 509)是理解递归和动态规划的最佳入门案例。定义很简单:F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)。但用DP实现时有几个关键点:
- DP数组定义:dp[i]表示第i个斐波那契数
- 递推公式:直接对应定义dp[i] = dp[i-1] + dp[i-2]
- 初始化:dp[0]=0, dp[1]=1必须显式初始化
- 遍历顺序:从i=2开始正向遍历,因为每个数依赖前两个数
- 空间优化:实际上只需要维护前两个状态,可将空间复杂度从O(n)降到O(1)
注意:C++中可变长度数组(VLA)不是标准特性,建议使用vector替代
cpp复制int fib(int n) {
if(n <= 1) return n;
vector<int> dp(n+1);
dp[0] = 0; dp[1] = 1;
for(int i = 2; i <= n; ++i)
dp[i] = dp[i-1] + dp[i-2];
return dp[n];
}
2.2 爬楼梯问题:斐波那契的变形
爬楼梯(LeetCode 70)看似与斐波那契无关,但分析后会发现它们共享相同的递推结构:
- 到第n阶的方法数 = 到第n-1阶的方法数(最后迈1步) + 到第n-2阶的方法数(最后迈2步)
- 边界条件:dp[1]=1(1阶只有1种方法),dp[2]=2(2阶可以1+1或直接2)
cpp复制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];
}
空间优化版(只维护前两个状态):
cpp复制int climbStairs(int n) {
if(n <= 2) return n;
int prev1 = 1, prev2 = 2;
for(int i = 3; i <= n; ++i) {
int curr = prev1 + prev2;
prev1 = prev2;
prev2 = curr;
}
return prev2;
}
2.3 最小花费爬楼梯:带权值的进阶版
最小花费爬楼梯(LeetCode 746)在前一题基础上增加了成本维度:
- dp[i]表示到达第i阶的最小花费
- 递推公式:dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2])
- 初始状态:dp[0]=dp[1]=0(地面和第一阶不花费)
cpp复制int minCostClimbingStairs(vector<int>& cost) {
vector<int> dp(cost.size()+1);
dp[0] = dp[1] = 0;
for(int i = 2; i <= cost.size(); ++i) {
dp[i] = min(dp[i-1] + cost[i-1],
dp[i-2] + cost[i-2]);
}
return dp[cost.size()];
}
3. DP解题的通用模式
通过这三道题,可以总结出DP问题的通用解法框架:
- 定义状态:明确dp[i]代表什么含义
- 状态转移:找出dp[i]与之前状态的关系式
- 边界条件:确定最小子问题的解
- 计算顺序:保证计算当前状态时依赖的状态已计算
- 空间优化:根据状态依赖关系压缩存储空间
4. 常见错误与调试技巧
4.1 初始化陷阱
在爬楼梯问题中,容易错误初始化dp[0]=1。实际上:
- dp[0]无实际意义(从地面到地面不算一步)
- dp[1]=1(只有1种方式)
- dp[2]=2(1+1或直接2)
4.2 索引越界问题
处理cost数组时要注意:
- cost数组长度为n时,楼梯顶部是n+1阶
- 访问cost[i-1]和cost[i-2]时要确保索引有效
4.3 空间复杂度优化
当递推公式只依赖有限个前驱状态时:
- 斐波那契和爬楼梯问题只需维护前两个状态
- 最小花费问题同样只需维护dp[i-1]和dp[i-2]
优化后的空间复杂度从O(n)降到O(1):
cpp复制int minCostClimbingStairs(vector<int>& cost) {
int prev1 = 0, prev2 = 0;
for(int i = 2; i <= cost.size(); ++i) {
int curr = min(prev1 + cost[i-1], prev2 + cost[i-2]);
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
5. 从这三道题中学到什么
- 识别DP问题特征:问题可以分解为重叠子问题,且具有最优子结构性质
- 建立状态转移思维:当前状态如何由之前状态推导而来
- 边界条件的重要性:DP的正确性往往取决于初始条件的正确设置
- 空间优化意识:根据状态依赖关系优化存储方案
这三道题虽然简单,但完美诠释了动态规划的核心思想。它们就像乐高积木的基础零件,掌握了这些基本模式,才能搭建更复杂的DP解决方案,比如后续的背包问题、编辑距离等。
