leetcode-master/problems/周总结/20210225动规周末总结.md

306 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

本周我们主要讲解了打家劫舍系列这个系列也是dp解决的经典问题那么来看看我们收获了哪些呢一起来回顾一下吧。
## 周一
[动态规划:开始打家劫舍!](https://programmercarl.com/0198.打家劫舍.html)中就是给一个数组相邻之间不能连着偷,如何偷才能得到最大金钱。
1. 确定dp数组含义
**dp[i]考虑下标i包括i以内的房屋最多可以偷窃的金额为dp[i]**
2. 确定递推公式
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
3. dp数组如何初始化
```
vector<int> dp(nums.size());
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
```
4. 确定遍历顺序
从前到后遍历
5. 举例推导dp数组
以示例二,输入[2,7,9,3,1]为例。
![198.打家劫舍](https://img-blog.csdnimg.cn/20210221170954115.jpg)
红框dp[nums.size() - 1]为结果。
## 周二
[动态规划:继续打家劫舍!](https://programmercarl.com/0213.打家劫舍II.html)就是数组成环了,然后相邻的不能连着偷。
这里主要考虑清楚三种情况:
* 情况一:考虑不包含首尾元素
![213.打家劫舍II](https://img-blog.csdnimg.cn/20210129160748643.jpg)
* 情况二:考虑包含首元素,不包含尾元素
![213.打家劫舍II1](https://img-blog.csdnimg.cn/20210129160821374.jpg)
* 情况三:考虑包含尾元素,不包含首元素
![213.打家劫舍II2](https://img-blog.csdnimg.cn/20210129160842491.jpg)
需要注意的是,**“考虑” 不等于 “偷”**例如情况三虽然是考虑包含尾元素但不一定要选尾部元素对于情况三取nums[1] 和 nums[3]就是最大的。
所以情况二 和 情况三 都包含了情况一了,**所以只考虑情况二和情况三就可以了**。
成环之后还是难了一些的, 不少题解没有把“考虑房间”和“偷房间”说清楚。
这就导致大家会有这样的困惑:“情况三怎么就包含了情况一了呢?本文图中最后一间房不能偷啊,偷了一定不是最优结果”。
所以我在本文重点强调了情况一二三是“考虑”的范围,而具体房间偷与不偷交给递推公式去抉择。
剩下的就和[动态规划:开始打家劫舍!](https://programmercarl.com/0198.打家劫舍.html)是一个逻辑了。
## 周三
[动态规划:还要打家劫舍!](https://programmercarl.com/0337.打家劫舍III.html)这次是在一棵二叉树上打家劫舍了,条件还是一样的,相临的不能偷。
这道题目是树形DP的入门题目其实树形DP其实就是在树上进行递推公式的推导没有什么神秘的。
这道题目我给出了暴力的解法:
```CPP
class Solution {
public:
int rob(TreeNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return root->val;
// 偷父节点
int val1 = root->val;
if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left相当于不考虑左孩子了
if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right相当于不考虑右孩子了
// 不偷父节点
int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子
return max(val1, val2);
}
};
```
当然超时了因为我们计算了root的四个孙子左右孩子的孩子为头结点的子树的情况又计算了root的左右孩子为头结点的子树的情况计算左右孩子的时候其实又把孙子计算了一遍。
那么使用一个map把计算过的结果保存一下这样如果计算过孙子了那么计算孩子的时候可以复用孙子节点的结果。
代码如下:
```CPP
class Solution {
public:
unordered_map<TreeNode* , int> umap; // 记录计算过的结果
int rob(TreeNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return root->val;
if (umap[root]) return umap[root]; // 如果umap里已经有记录则直接返回
// 偷父节点
int val1 = root->val;
if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left
if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right
// 不偷父节点
int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子
umap[root] = max(val1, val2); // umap记录一下结果
return max(val1, val2);
}
};
```
最后我们还是给出动态规划的解法。
因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解。
1. 确定递归函数的参数和返回值
```CPP
vector<int> robTree(TreeNode* cur) {
```
dp数组含义下标为0记录不偷该节点所得到的的最大金钱下标为1记录偷该节点所得到的的最大金钱。
**所以本题dp数组就是一个长度为2的数组**
那么有同学可能疑惑长度为2的数组怎么标记树中每个节点的状态呢
**别忘了在递归的过程中,系统栈会保存每一层递归的参数**
2. 确定终止条件
在遍历的过程中如果遇到空间点的话很明显无论偷还是不偷都是0所以就返回
```
if (cur == NULL) return vector<int>{0, 0};
```
3. 确定遍历顺序
采用后序遍历,代码如下:
```CPP
// 下标0不偷下标1
vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右
// 中
```
4. 确定单层递归的逻辑
如果是偷当前节点那么左右孩子就不能偷val1 = cur->val + left[0] + right[0];
如果不偷当前节点那么左右孩子就可以偷至于到底偷不偷一定是选一个最大的所以val2 = max(left[0], left[1]) + max(right[0], right[1]);
最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}
代码如下:
```CPP
vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右
// 偷cur
int val1 = cur->val + left[0] + right[0];
// 不偷cur
int val2 = max(left[0], left[1]) + max(right[0], right[1]);
return {val2, val1};
```
5. 举例推导dp数组
以示例1为例dp数组状态如下**注意用后序遍历的方式推导**
![337.打家劫舍III](https://img-blog.csdnimg.cn/20210129181331613.jpg)
**最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱**
树形DP为什么比较难呢
因为平时我们习惯了在一维数组或者二维数组上推导公式,一下子换成了树,就需要对树的遍历方式足够了解!
大家还记不记得我在讲解贪心专题的时候,讲到这道题目:[贪心算法:我要监控二叉树!](https://programmercarl.com/0968.监控二叉树.html),这也是贪心算法在树上的应用。**那我也可以把这个算法起一个名字,叫做树形贪心**,哈哈哈
“树形贪心”词汇从此诞生,来自「代码随想录」
## 周四
[动态规划:买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html) 一段时间,只能买卖一次,问最大收益。
这里我给出了三种解法:
暴力解法代码:
```CPP
class Solution {
public:
int maxProfit(vector<int>& prices) {
int result = 0;
for (int i = 0; i < prices.size(); i++) {
for (int j = i + 1; j < prices.size(); j++){
result = max(result, prices[j] - prices[i]);
}
}
return result;
}
};
```
* 时间复杂度O(n^2)
* 空间复杂度O(1)
贪心解法代码如下:
因为股票就买卖一次,那么贪心的想法很自然就是取最左最小值,取最右最大值,那么得到的差值就是最大利润。
```CPP
class Solution {
public:
int maxProfit(vector<int>& prices) {
int low = INT_MAX;
int result = 0;
for (int i = 0; i < prices.size(); i++) {
low = min(low, prices[i]); // 取最左最小价格
result = max(result, prices[i] - low); // 直接取最大区间利润
}
return result;
}
};
```
* 时间复杂度O(n)
* 空间复杂度O(1)
动规解法,版本一,代码如下:
```CPP
// 版本一
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(len, vector<int>(2));
dp[0][0] -= prices[0];
dp[0][1] = 0;
for (int i = 1; i < len; i++) {
dp[i][0] = max(dp[i - 1][0], -prices[i]);
dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
}
return dp[len - 1][1];
}
};
```
* 时间复杂度O(n)
* 空间复杂度O(n)
从递推公式可以看出dp[i]只是依赖于dp[i - 1]的状态。
那么我们只需要记录 当前天的dp状态和前一天的dp状态就可以了可以使用滚动数组来节省空间代码如下
```CPP
// 版本二
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(2, vector<int>(2)); // 注意这里只开辟了一个2 * 2大小的二维数组
dp[0][0] -= prices[0];
dp[0][1] = 0;
for (int i = 1; i < len; i++) {
dp[i % 2][0] = max(dp[(i - 1) % 2][0], -prices[i]);
dp[i % 2][1] = max(dp[(i - 1) % 2][1], prices[i] + dp[(i - 1) % 2][0]);
}
return dp[(len - 1) % 2][1];
}
};
```
* 时间复杂度O(n)
* 空间复杂度O(1)
建议先写出版本一,然后在版本一的基础上优化成版本二,而不是直接就写出版本二。
## 总结
刚刚结束了背包问题,本周主要讲解打家劫舍系列。
**劫舍系列简单来说就是 数组上连续元素二选一,成环之后连续元素二选一,在树上连续元素二选一,所能得到的最大价值**
那么这里每一种情况 我在文章中都做了详细的介绍。
周四我们开始讲解股票系列了,大家应该预测到了,我们下周的主题就是股票! 哈哈哈,多么浮躁的一个系列!敬请期待吧!
**代码随想录温馨提醒:投资有风险,入市需谨慎!**
<div align="center"><img src=https://code-thinking.cdn.bcebos.com/pics/01二维码.jpg width=450> </img></div>