理论基础
无论大家之前对动态规划学到什么程度,一定要先看 我讲的 动态规划理论基础。 如果没做过动态规划的题目,看我讲的理论基础,会有感觉 是不是简单题想复杂了? 其实并没有,我讲的理论基础内容,在动规章节所有题目都有运用,所以很重要! 如果做过动态规划题目的录友,看我的理论基础 就会感同身受了。
文章:
视频:从此再也不怕动态规划了,动态规划解题方法论大曝光 !| 理论基础 |力扣刷题总结| 动态规划入门_哔哩哔哩_bilibili
如果某一问题有很多重叠子问题,使用动态规划是最有效的。动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。
动态规划问题五步曲:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
——递推公式很重要,但是dp数组的初始化以及正确的遍历顺序也很重要。
做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。如果代码没通过就打印dp数组,动态规划debug最好的方式是把dp数组打印出来,看看究竟是不是按照自己思路推导的。
通过不了,就思考这三个问题:
- 这道题目我举例推导状态转移公式了么?
- 我打印dp数组的日志了么?
- 打印出来了dp数组和我想的一样么?
509. 斐波那契数
很简单的动规入门题,但简单题使用来掌握方法论的,还是要有动规五部曲来分析。
文章:
这道题比较简单,主要是熟悉一下动规五部曲。对于这道题,可以指维护两个值,因为只要知道前一个数和前前一个数即可算出。除此之外还可以使用递归函数来解决。
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];
}
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];
}
70. 爬楼梯
本题大家先自己想一想, 之后会发现,和 斐波那契数 有点关系。
文章:
这道题就是要考虑到dp数组的意义是每一层的方法数,这样问题就迎刃而解,当前楼层的方法数就是前一层的方法数+前前一层的方法数(因为这道题一次只能上一个或两个台阶,所以只有这层楼的前一个和前前一个才能直接登上这层楼,考虑的是哪一层是倒数第二层,而倒数第二层只能有一种方法登上这一层(并不是说经过前前一层只能有一种方法到达这一层,实际上并不是,而是以前前一层为倒数第二层这样直接到达当前层,考虑前一种的话,实际上是将dp[n-2]的一部分也算进去了,就重复了),故而其实和斐波那契数列很相似了。(所以也可以像前一道题一样简化)
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];
}
这是文章里面的拓展,题目变成了一次不一定迈多少阶楼梯,其实大差不差,需要考虑到这一层的方法数是从迈最大步子能一步到达这一层的楼层的方法数累加起来,和上面一个道理。
int climbStairs(int n) {
vector<int> dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) { // 把m换成2,就可以AC爬楼梯这道题
if (i - j >= 0) dp[i] += dp[i - j];
}
}
return dp[n];
}
746. 使用最小花费爬楼梯
这道题目力扣改了题目描述了,现在的题目描述清晰很多,相当于明确说 第一步是不用花费的。 更改题目描述之后,相当于是 文章中 「拓展」的解法
文章:
- dp数组的意义稍有区别,代表到达每一层的花费,注意dp数组的长度是cost长度+1,这样dp[n]才能代表到达第cost.size()+1层的花费。
- 递推公式发生了改变,要取min(到达前一层/ 前前一层的花费 + 从前一层/ 前前一层到达这一层的花费),取最小花费。
- 初始化也改变,这道题相当于是从0层开始上,从0层开始到达第一/ 二层都是不花费的,但是从第一/ 二层往下一层走就要花费了,相当于买路钱,留了买路钱才能从这里过,但是如果不过去,只是到达这里的话,是不需要花费的。
- 最后要返回dp数组的最后一个值,也就是到达最后一层的最小花费。
- 同样因为当前dp数组值推导只需要它的前两个值,所以也可以化简。
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()];
}
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;
}
拓展的解法其实就是换了dp数组的意义,但是还是能通过的。
注意到这次dp数组的长度是cost.size()了,是因为这次dp数组不再是到达本层需要的花费了,而是到达本层而且还要往上走的花费,所以最后返回的不直接是是dp数组的值了,而是最后二者取较小者(到达顶层之后就不需要往上走了)。
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]);
}