理论基础
动态规划五部曲:
- 确定dp数组下标及值的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序(遍历循环外层是啥内层是啥,遍历从左向右还是从右向左)
- 打印dp数组,用于 debug
509.斐波那契数
严格按照动态规划五部曲进行思考分析!
1、确定dp数组下标及值的含义
dp[i]:下标 i 表示第 i 个 Fibonacci 数,dp[i] 的值表示第 i 个 Fibonacci 数的数值为 dp[i](i 从 0 开始)
2、确定递推公式
题目描述其实已经告诉我们递推公式了:dp[i] = dp[i - 1] + dp[i - 2]
3、dp 数组初始化
根据递推公式,至少需要初始化两个数值。题目也告诉我们了:dp[0] = 1, dp[1] = 1
4、确定遍历顺序
从递归公式 dp[i] = dp[i - 1] + dp[i - 2] 中可以看出,dp[i] 是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的,这样 dp[i] 所依赖的 dp[i - 1] 和 dp[i - 2] 才是已经被更新过的值
5、打印 dp 数组验证
注意,上面是我们的思考过程,如何初始化是依赖于我们的递归公式的。但是在代码实现中,需要先初始化,再利用递推公式更新 dp 数组
代码如下
class Solution {
public:
int fib(int n) {
if (n <= 1) return n;
vector<int> dp(n + 1); // i表示第i个Fibonacci数,dp[i]表示第i个Fibonacci数的值
// 递推公式从题目中获得:dp[i] = dp[i - 1] + dp[i - 2]
// 初始化从题目中获得:dp[0] = 0, dp[1] = 1;
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; ++i) { // 遍历顺序从前向后
dp[i] = dp[i - 1] + dp[i - 2];
}
// 打印dp数组验证,略
return dp[n]; // 根据题意与dp数组的含义,最后的结果就是dp[n]的值
}
};
70.爬楼梯
1、确定 dp 数组下标及值的含义
dp[i]:下标 i 表示位于第 i 阶楼梯,dp[i] 的值表示爬到第 i 阶楼梯的方法数为 dp[i]
2、确定递推公式
想求dp[i],这个 i 表示位于第 i 阶楼梯,根据题目描述,可以从第 i - 1 阶爬一步到第 i 阶,也可以从第 i - 2 阶爬两步到第 i 阶。因此,爬到第 i 阶楼梯的方法数 dp[i] 等于爬到第 i - 1 阶楼梯的方法数 dp[i - 1] 加上爬到第 i - 2 阶楼梯的方法数 dp[i - 2],即 dp[i] = dp[i - 1] + dp[i - 2]
在推导 dp[i] 的时候,一定要时刻想着 dp 数组下标及值的含义,否则容易跑偏
3、dp 数组初始化
不考虑 dp[0] 如何初始化,只初始化 dp[1] = 1,dp[2] = 2,然后从 i = 3 开始递推,这样才符合 dp[i] 的定义
dp[1],即爬到第 1 阶的方法数应该为 1,dp[1] = 1
dp[2],即爬到第 2 阶的方法数应该为 2(一次爬 1 阶或者直接爬 2 阶),dp[2] = 2
4、确定遍历顺序
从递归公式 dp[i] = dp[i - 1] + dp[i - 2] 中可以看出,dp[i] 是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的,这样 dp[i] 所依赖的 dp[i - 1] 和 dp[i - 2] 才是已经被更新过的值
5、打印 dp 数组验证
代码如下
class Solution {
public:
int climbStairs(int n) {
if (n == 1) return 1;
// 1. 确定dp数组下标及值的含义:下标i表示位于第i个台阶,dp[i]表示爬到第i个台阶的方法数
vector<int> dp(n + 1);
// 2. 确定递推公式:dp[i] = dp[i-1] + dp[i-2]
// 3. 初始化dp数组:我们不考虑dp[0]如何初始化,只考虑初始化dp[1]和dp[2],后续利用递归公式更新dp数组时从下标3开始更新即可
dp[1] = 1;
dp[2] = 2;
// 4. 确定遍历顺序:从前向后遍历,这样才能够利用前两个已被更新的值
for (int i = 3; i <= n; ++i)
dp[i] = dp[i - 1] + dp[i - 2];
// 5. 打印dp数组验证,略
return dp[n]; // 根据题意与dp数组的含义,最后的结果就是dp[n]的值
}
};
746.使用最小花费爬楼梯
注意本题跳的时候才需要花费
1、确定 dp 数组下标及值的含义
dp[i]:下标 i 表示位于第 i 个台阶,dp[i] 的值表示爬到第 i 个台阶的最小花费为 dp[i]
2、确定递推公式
想求dp[i],这个 i 表示位于第 i 个台阶,根据题目描述,可以从第 i - 1 阶爬一步到第 i 阶,也可以从第 i - 2 阶爬两步到第 i 阶。因此,爬到第 i 阶的最小花费 dp[i] 等于爬到第 i - 1 阶的最小花费 dp[i - 1] 加上 cost[i - 1] 与爬到第 i - 2 阶的最小花费 dp[i - 2] 加上 cost[i - 2] 中的最小值,即 dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
在推导 dp[i] 的时候,一定要时刻想着 dp 数组下标及值的含义,否则容易跑偏
3、dp 数组初始化
根据递推公式,至少需要初始化两个数值
可以选择从下标 0 的台阶开始跳,故爬到台阶 0 所需要的最小花费为 0,即 dp[0] = 0
也可以选择从下标 1 的台阶开始跳,故爬到台阶 1 所需要的最小花费为 0,即 dp[1] = 0
4、确定遍历顺序
从递归公式可以看出,dp[i] 依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的,这样 dp[i] 所依赖的 dp[i - 1] 和 dp[i - 2] 才是已经被更新过的值
5、打印 dp 数组验证
代码如下
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
// 1、确定dp[i]含义:i表示位于台阶i,dp[i]表示到达台阶i的最小花费
vector<int> dp(cost.size() + 1);
// 2、确定递推公式:从台阶i-1能够爬到台阶i,从台阶i-2也能够爬到台阶i,到达台阶i的最小花费为这两种方案花费的最小值。dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
// 3、初始化,根据题意,dp[0] = dp[1] = 0;
dp[0] = dp[1] = 0;
// 4、确定遍历顺序,从前向后遍历
for (int i = 2; i <= cost.size(); ++i)
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
// 5、打印dp数组略
return dp[cost.size()]; // 根据题意,结果就是dp[cost.size()]
}
};
回顾总结
动态规划五部曲的思维模式
注意我们确定dp数组的下标及值的含义时,下标含义与值的含义是分开定义的,想想为什么要这样(提示两个字:状态)