目录
为什么要先确定递推公式,然后在考虑初始化呢?因为一些情况是递推公式决定了dp数组要如何初始化!
找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!
做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。
然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。
如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。
如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。
可以自己先思考这三个问题:
如果这灵魂三问自己都做到了,基本上这道题目也就解决了,或者更清晰的知道自己究竟是哪一点不明白,是状态转移不明白,还是实现代码不知道该怎么写,还是不理解遍历dp数组的顺序。
注意这里不是说不让大家问问题哈, 而是说问问题之前要有自己的思考,问题要问到点子上!
- class Solution {
- public:
- int fib(int n) {
- if(n<2) return n;
- else return fib(n-1)+fib(n-2);
- }
- };
动规五部曲:这里要用一个一维dp数组来保存递归的结果
dp[i]的定义为:第i个数的斐波那契数值是dp[i]
题目已经把递推公式直接给我们了:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2]
题目中把如何初始化也直接给了
- dp[0] = 0;
- dp[1] = 1;
从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列:
0 1 1 2 3 5 8 13 21 34 55
如果代码写出来,发现结果不对,就把dp数组打印出来看看和我们推导的数列是不是一致的。
- class Solution {
- public:
- 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];
- }
- };
当然可以发现,只需要维护两个数值就可以了,不需要记录整个序列。
代码如下:
- class Solution {
- public:
- int fib(int N) {
- if (N <= 1) return N;
- int dp[2];
- dp[0] = 0;
- dp[1] = 1;
- for (int i = 2; i <= N; i++) {
- int sum = dp[0] + dp[1];
- dp[0] = dp[1];
- dp[1] = sum;
- }
- return dp[1];
- }
- };
暗戳戳说,感觉这道题跟上面一道题一样啊,就是有了实际背景!
动规五部曲:
定义一个一维数组来记录不同楼层的状态
1)确定dp数组以及下标的含义
dp[i]: 爬到第i层楼梯,有dp[i]种方法
2)确定递推公式
从dp[i]的定义可以看出,dp[i] 可以有两个方向推出来。
首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。
还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶不就是dp[i]了么。
那么dp[i]就是 dp[i - 1]与dp[i - 2]之和!
所以dp[i] = dp[i - 1] + dp[i - 2] 。
3)dp数组如何初始化
不考虑dp[0]如何初始化,只初始化dp[1] = 1,dp[2] = 2,然后从i = 3开始递推,这样才符合dp[i]的定义。(dp[0]不管怎么定义都很牵强,那就不定义了,就是这么任性!嘻嘻!)
4)确定遍历顺序
从递推公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,遍历顺序一定是从前向后遍历的
5)举例推导dp数组
举例当n为5的时候,dp table(dp数组)应该是这样的

以上五部分析完之后,C++代码如下:
- // 版本一
- class Solution {
- public:
- int climbStairs(int n) {
- if (n <= 1) return n; // 因为下面直接对dp[2]操作了,防止空指针
- vector<int> dp(n + 1);
- dp[1] = 1;
- dp[2] = 2;
- for (int i = 3; i <= n; i++) { // 注意i是从3开始的
- dp[i] = dp[i - 1] + dp[i - 2];
- }
- return dp[n];
- }
- };
当然依然也可以,优化一下空间复杂度,代码如下:
- // 版本二
- class Solution {
- public:
- int climbStairs(int n) {
- if (n <= 1) return n;
- int dp[3];
- dp[1] = 1;
- dp[2] = 2;
- for (int i = 3; i <= n; i++) {
- int sum = dp[1] + dp[2];
- dp[1] = dp[2];
- dp[2] = sum;
- }
- return dp[2];
- }
- };
后面的很多动规的题目其实都是当前状态依赖前两个,或者前三个状态,都可以做空间上的优化,但面试中能写出版本一就够了哈,清晰明了,如果面试官要求进一步优化空间的话,再去优化。因为版本一才能体现出动规的思想精髓,递推的状态变化。
1)确定dp数组以及下标的含义
使用动态规划,就要有一个数组来记录状态,本题只需要一个一维数组dp[i]就可以了。
dp[i]的定义:到达第i台阶所花费的最少体力为dp[i]。
2)确定递推公式
可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]。
dp[i - 1] 跳到 dp[i] 需要花费 dp[i - 1] + cost[i - 1]。
dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2]。
那么究竟是选从dp[i - 1]跳还是从dp[i - 2]跳呢?
一定是选最小的,所以dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
3)dp数组如何初始化
看一下递归公式,dp[i]由dp[i - 1],dp[i - 2]推出,既然初始化所有的dp[i]是不可能的,那么只初始化dp[0]和dp[1]就够了,其他的最终都是dp[0]dp[1]推出。
那么 dp[0] 应该是多少呢? 根据dp数组的定义,到达第0台阶所花费的最小体力为dp[0],那么有同学可能想,那dp[0] 应该是 cost[0],例如 cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] 的话,dp[0] 就是 cost[0] 应该是1。
这里就要说明本题力扣为什么改题意,而且修改题意之后就清晰很多的原因了。
新题目描述中明确说了 “你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。” 也就是说 到达 第 0 个台阶是不花费的,但从 第0 个台阶 往上跳的话,需要花费 cost[0]。
所以初始化 dp[0] = 0,dp[1] = 0;
4)确定遍历顺序
最后一步,递归公式有了,初始化有了,如何遍历呢?
本题的遍历顺序其实比较简单,简单到很多同学都忽略了思考这一步直接就把代码写出来了。
因为是模拟台阶,而且dp[i]由dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了。
但是稍稍有点难度的动态规划,其遍历顺序并不容易确定下来。 例如:01背包,都知道两个for循环,一个for遍历物品嵌套一个for遍历背包容量,那么为什么不是一个for遍历背包容量嵌套一个for遍历物品呢? 以及在使用一维dp数组的时候遍历背包容量为什么要倒序呢?
这些都与遍历顺序息息相关。
5)举例推导dp数组
拿示例2:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] ,来模拟一下dp数组的状态变化,如下:

以上分析完毕,整体C++代码如下:
- class Solution {
- public:
- int minCostClimbingStairs(vector<int>& cost) {
- vector<int> dp(cost.size() + 1);
- dp[0] = 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()];
- }
- };
还可以优化空间复杂度,因为dp[i]就是由前两位推出来的,那么也不用dp数组了,C++代码如下:
- // 版本二
- class Solution {
- public:
- int minCostClimbingStairs(vector<int>& cost) {
- int dp0 = 0;
- int dp1 = 0;
- for (int i = 2; i <= cost.size(); i++) {
- int dpi = min(dp1 + cost[i - 1], dp0 + cost[i - 2]);
- dp0 = dp1; // 记录一下前两位
- dp1 = dpi;
- }
- return dp1;
- }
- };
旧力扣描述,如果按照第一步是花费的,最后一步不花费,那么代码是这么写的
- // 版本一
- class Solution {
- public:
- int minCostClimbingStairs(vector<int>& cost) {
- vector<int> dp(cost.size());
- dp[0] = cost[0]; // 第一步有花费
- dp[1] = cost[1];
- for (int i = 2; i < cost.size(); i++) {
- dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
- }
- // 注意最后一步可以理解为不用花费,所以取倒数第一步,第二步的最少值
- return min(dp[cost.size() - 1], dp[cost.size() - 2]);
- }
- };