
本周我们主要讲解了打家劫舍系列，这个系列也是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>
