更新动态规划专题Markdown文件

This commit is contained in:
youngyangyang04
2021-05-10 17:07:39 +08:00
parent 8c9e147998
commit 8072aac750
56 changed files with 9063 additions and 63 deletions

View File

@ -0,0 +1,151 @@
这周我们正式开始动态规划的学习!
## 周一
在[关于动态规划,你该了解这些!](https://mp.weixin.qq.com/s/ocZwfPlCWrJtVGACqFNAag)中我们讲解了动态规划的基础知识。
首先讲一下动规和贪心的区别,其实大家不用太强调理论上的区别,做做题,就感受出来了。
然后我们讲了动规的五部曲:
1. 确定dp数组dp table以及下标的含义
2. 确定递推公式
3. dp数组如何初始化
4. 确定遍历顺序
5. 举例推导dp数组
后序我们在讲解动规的题目时候,都离不开这五步!
本周都是简单题目,大家可能会感觉 按照这五部来好麻烦,凭感觉随手一写,直接就过,越到后面越会感觉,凭感觉这个事还是不靠谱的,哈哈。
最后我们讲了动态规划题目应该如何debug相信一些录友做动规的题目一旦报错也是凭感觉来改。
其实只要把dp数组打印出来哪里有问题一目了然
**如果代码写出来了一直AC不了灵魂三问**
1. 这道题目我举例推导状态转移公式了么?
2. 我打印dp数组的日志了么
3. 打印出来了dp数组和我想的一样么
哈哈专治各种代码写出来了但AC不了的疑难杂症。
## 周二
这道题目[动态规划:斐波那契数](https://mp.weixin.qq.com/s/ko0zLJplF7n_4TysnPOa_w)是当之无愧的动规入门题。
简单题我们就是用来了解方法论的用动规五部曲走一遍题目其实已经把递推公式和dp数组如何初始化都给我们了。
## 周三
[动态规划:爬楼梯](https://mp.weixin.qq.com/s/Ohop0jApSII9xxOMiFhGIw) 这道题目其实就是斐波那契数列。
但正常思考过程应该是推导完递推公式之后,发现这是斐波那契,而不是上来就知道这是斐波那契。
在这道题目的第三步确认dp数组如何初始化其实就可以看出来对dp[i]定义理解的深度。
dp[0]其实就是一个无意义的存在不用去初始化dp[0]。
有的题解是把dp[0]初始化为1然后遍历的时候i从2开始遍历这样是可以解题的然后强行解释一波dp[0]应该等于1的含义。
一个严谨的思考过程应该是初始化dp[1] = 1dp[2] = 2然后i从3开始遍历代码如下
```C++
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) { // 注意i是从3开始的
dp[i] = dp[i - 1] + dp[i - 2];
}
```
这个可以是面试的一个小问题哈哈考察候选人对dp[i]定义的理解程度。
这道题目还可以继续深化,就是一步一个台阶,两个台阶,三个台阶,直到 m个台阶有多少种方法爬到n阶楼顶。
这又有难度了,这其实是一个完全背包问题,但力扣上没有这种题目,所以后续我在讲解背包问题的时候,今天这道题还会拿从背包问题的角度上来再讲一遍。
这里我先给出我的实现代码:
```C++
class Solution {
public:
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];
}
};
```
代码中m表示最多可以爬m个台阶。
**以上代码不能运行哈我主要是为了体现只要把m换成2粘过去就可以AC爬楼梯这道题不信你就粘一下试试哈哈**。
**此时我就发现一个绝佳的大厂面试题**第一道题就是单纯的爬楼梯然后看候选人的代码实现如果把dp[0]的定义成1了就可以发难了为什么dp[0]一定要初始化为1此时可能候选人就要强行给dp[0]应该是1找各种理由。那这就是一个考察点了对dp[i]的定义理解的不深入。
然后可以继续发难,如果一步一个台阶,两个台阶,三个台阶,直到 m个台阶有多少种方法爬到n阶楼顶。这道题目leetcode上并没有原题绝对是考察候选人算法能力的绝佳好题。
这一连套问下来,候选人算法能力如何,面试官心里就有数了。
**其实大厂面试最喜欢问题的就是这种简单题,然后慢慢变化,在小细节上考察候选人**。
这道绝佳的面试题我没有用过,如果录友们有面试别人的需求,就把这个套路拿去吧,哈哈哈。
我在[通过一道面试题目,讲一讲递归算法的时间复杂度!](https://mp.weixin.qq.com/s/I6ZXFbw09NR31F5CJR_geQ)中以我自己面试别人的真实经历通过求x的n次方 这么简单的题目,就可以考察候选人对算法性能以及递归的理解深度,录友们可以看看,绝对有收获!
## 周四
这道题目[动态规划:使用最小花费爬楼梯](https://mp.weixin.qq.com/s/djZB9gkyLFAKcQcSvKDorA)就是在爬台阶的基础上加了一个花费,
这道题描述也确实有点魔幻。
题目描述为:每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。
示例1
输入cost = [10, 15, 20]
输出15
**从题目描述可以看出:要不是第一步不需要花费体力,要不就是第最后一步不需要花费体力,我个人理解:题意说的其实是第一步是要支付费用的!**。因为是当你爬上一个台阶就要花费对应的体力值!
所以我定义的dp[i]意思是也是第一步是要花费体力的,最后一步不用花费体力了,因为已经支付了。
之后一些录友在留言区说 可以定义dp[i]为:第一步是不花费体力,最后一步是花费体力的。
所以代码也可以这么写:
```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()];
}
};
```
这么写看上去比较顺,但是就是感觉和题目描述的不太符。哈哈,也没有必要这么细扣题意了,大家只要知道,题目的意思反正就是要不是第一步不花费,要不是最后一步不花费,都可以。
## 总结
本周题目简单一些,也非常合适初学者来练练手。
下周开始上难度了哈,然后大下周就开始讲解背包问题,好戏还在后面,录友们跟上哈。
学算法认准「代码随想录」就够了Carl带你打怪升级

View File

@ -0,0 +1,159 @@
## 周一
[动态规划:不同路径](https://mp.weixin.qq.com/s/MGgGIt4QCpFMROE9X9he_A)中求从出发点到终点有几种路径,只能向下或者向右移动一步。
我们提供了三种方法,但重点讲解的还是动规,也是需要重点掌握的。
**dp[i][j]定义 表示从0 0出发到(i, j) 有dp[i][j]条不同的路径**
本题在初始化的时候需要点思考了,即:
dp[i][0]一定都是1因为从(0, 0)的位置到(i, 0)的路径只有一条那么dp[0][j]也同理。
所以初始化为:
```
for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;
```
这里已经不像之前做过的题目随便赋个0就行的。
遍历顺序以及递推公式:
```
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
```
![62.不同路径1](https://img-blog.csdnimg.cn/20201209113631392.png)
## 周二
[动态规划:不同路径还不够,要有障碍!](https://mp.weixin.qq.com/s/lhqF0O4le9-wvalptOVOww)相对于[动态规划:不同路径](https://mp.weixin.qq.com/s/MGgGIt4QCpFMROE9X9he_A)添加了障碍。
dp[i][j]定义依然是表示从0 0出发到(i, j) 有dp[i][j]条不同的路径。
本题难点在于初始化,如果(i, 0) 这条边有了障碍之后障碍之后包括障碍都是走不到的位置了所以障碍之后的dp[i][0]应该还是初始值0。
如图:
![63.不同路径II](https://img-blog.csdnimg.cn/20210104114513928.png)
这里难住了不少同学,代码如下:
```
vector<vector<int>> dp(m, vector<int>(n, 0));
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;
```
递推公式只要考虑一下障碍,就不赋值了就可以了,如下:
```
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (obstacleGrid[i][j] == 1) continue;
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
```
拿示例1来举例如题
![63.不同路径II1](https://img-blog.csdnimg.cn/20210104114548983.png)
对应的dp table 如图:
![63.不同路径II2](https://img-blog.csdnimg.cn/20210104114610256.png)
## 周三
[动态规划:整数拆分,你要怎么拆?](https://mp.weixin.qq.com/s/cVbyHrsWH_Rfzlj-ESr01A)给出一个整数,问有多少种拆分的方法。
这道题目就有点难度了题目中dp我也给出了两种方法但通过两种方法的比较可以看出对dp数组定义的理解以及dp数组初始化的重要性。
**dp[i]定义分拆数字i可以得到的最大乘积为dp[i]**
本题中dp[i]的初始化其实也很有考究严格从dp[i]的定义来说dp[0] dp[1] 就不应该初始化,也就是没有意义的数值。
拆分0和拆分1的最大乘积是多少
这是无解的。
所以题解里我只初始化dp[2] = 1从dp[i]的定义来说拆分数字2得到的最大乘积是1这个没有任何异议
```
vector<int> dp(n + 1);
dp[2] = 1;
```
遍历顺序以及递推公式:
```
for (int i = 3; i <= n ; i++) {
for (int j = 1; j < i - 1; j++) {
dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
}
}
```
举例当n为10 的时候dp数组里的数值如下
![343.整数拆分](https://img-blog.csdnimg.cn/20210104173021581.png)
一些录友可能对为什么没有拆分j没有想清楚。
其实可以模拟一下哈拆分j的情况在遍历j的过程中dp[i - j]其实都计算过了。
例如 i= 10j = 5i-j = 5如果把j查分为 2 和 3其实在j = 2 的时候i-j= 8 拆分i-j的时候就可以拆出来一个3了。
**或者也可以理解j是拆分i的第一个整数**
[动态规划:整数拆分,你要怎么拆?](https://mp.weixin.qq.com/s/cVbyHrsWH_Rfzlj-ESr01A)总结里我也给出了递推公式dp[i] = max(dp[i], dp[i - j] * dp[j])这种写法。
对于这种写法一位录友总结的很好意思就是如果递推公式是dp[i-j] * dp[j],这样就相当于强制把一个数至少拆分成四份。
dp[i-j]至少是两个数的乘积dp[j]又至少是两个数的乘积但其实3以下的数数的本身比任何它的拆分乘积都要大了所以文章中初始化的时候才要特殊处理。
## 周四
[动态规划:不同的二叉搜索树](https://mp.weixin.qq.com/s/8VE8pDrGxTf8NEVYBDwONw)给出n个不同的节点求能组成多少个不同二叉搜索树。
这道题目还是比较难的,想到用动态规划的方法就很不容易了!
**dp[i]定义 1到i为节点组成的二叉搜索树的个数为dp[i]**
递推公式dp[i] += dp[j - 1] * dp[i - j]; j-1 为j为头结点左子树节点数量i-j 为以j为头结点右子树节点数量
dp数组如何初始化只需要初始化dp[0]就可以了推导的基础都是dp[0]。
n为5时候的dp数组状态如图
![96.不同的二叉搜索树3](https://img-blog.csdnimg.cn/20210107093253987.png)
## 总结
本周题目已经开始点难度了,特别是[动态规划:不同的二叉搜索树](https://mp.weixin.qq.com/s/8VE8pDrGxTf8NEVYBDwONw)这道题目,明显感觉阅读量很低,可能是因为确实有点难吧。
我现在也陷入了纠结,题目一简单,就会有录友和我反馈说题目太简单了,题目一难,阅读量就特别低。
我也好难那,哈哈哈。
**但我还会坚持规划好的路线,难度循序渐进,并以面试经典题目为准,该简单的时候就是简单,同时也不会因为阅读量低就放弃有难度的题目!**
录友们看到这是不是得给个Carl点个赞啊[让我看看]。
预告,我们下周正式开始讲解背包问题,经典的不能再经典,也是比较难的一类动态规划的题目了,录友们上车抓稳咯。

View File

@ -0,0 +1,168 @@
# 本周小结!(动态规划系列三)
本周我们正式开始讲解背包问题,也是动规里非常重要的一类问题。
背包问题其实有很多细节,如果了解个大概,然后也能一气呵成把代码写出来,但稍稍变变花样可能会陷入迷茫了。
开始回顾一下本周的内容吧!
## 周一
[动态规划关于01背包问题你该了解这些](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w)中,我们开始介绍了背包问题。
首先对于背包的所有问题中01背包是最最基础的其他背包也是在01背包的基础上稍作变化。
所以我才花费这么大精力去讲解01背包。
关于其他几种常用的背包,大家看这张图就了然于胸了:
![416.分割等和子集1](https://img-blog.csdnimg.cn/20210117171307407.png)
本文用动规五部曲详细讲解了01背包的二维dp数组的实现方法大家其实可以发现最简单的是推导公式了推导公式估计看一遍就记下来了但难就难在确定初始化和遍历顺序上。
1. 确定dp数组以及下标的含义
dp[i][j] 表示从下标为[0-i]的物品里任意取放进容量为j的背包价值总和最大是多少。
2. 确定递推公式
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
3. dp数组如何初始化
```C++
// 初始化 dp
vector<vector<int>> dp(weight.size() + 1, vector<int>(bagWeight + 1, 0));
for (int j = bagWeight; j >= weight[0]; j--) {
dp[0][j] = dp[0][j - weight[0]] + value[0];
}
```
4. 确定遍历顺序
**01背包二维dp数组在遍历顺序上外层遍历物品 ,内层遍历背包容量 和 外层遍历背包容量 ,内层遍历物品 都是可以的!**
但是先遍历物品更好理解。代码如下:
```C++
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j]; // 这个是为了展现dp数组里元素的变化
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
```
5. 举例推导dp数组
背包最大重量为4。
物品为:
| | 重量 | 价值 |
| --- | --- | --- |
| 物品0 | 1 | 15 |
| 物品1 | 3 | 20 |
| 物品2 | 4 | 30 |
来看一下对应的dp数组的数值如图
![动态规划-背包问题4](https://img-blog.csdnimg.cn/20210118163425129.jpg)
最终结果就是dp[2][4]。
## 周二
[动态规划关于01背包问题你该了解这些滚动数组](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA)中把01背包的一维dp数组滚动数组实现详细讲解了一遍。
分析一下和二维dp数组有什么区别在初始化和遍历顺序上又有什么差异
最后总结了一道朴实无华的背包面试题。
要求候选人先实现一个纯二维的01背包如果写出来了然后再问为什么两个for循环的嵌套顺序这么写反过来写行不行再讲一讲初始化的逻辑。
然后要求实现一个一维数组的01背包最后再问一维数组的01背包两个for循环的顺序反过来写行不行为什么
这几个问题就可以考察出候选人的算法功底了。
01背包一维数组分析如下
1. 确定dp数组的定义
在一维dp数组中dp[j]表示容量为j的背包所背的物品价值可以最大为dp[j]。
2. 一维dp数组的递推公式
```
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
```
3. 一维dp数组如何初始化
如果物品价值都是大于0的所以dp数组初始化的时候都初始为0就可以了。
4. 一维dp数组遍历顺序
代码如下:
```C++
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
```
5. 举例推导dp数组
一维dp分别用物品0物品1物品2 来遍历背包,最终得到结果如下:
![动态规划-背包问题9](https://img-blog.csdnimg.cn/20210110103614769.png)
## 周三
[动态规划416. 分割等和子集](https://mp.weixin.qq.com/s/sYw3QtPPQ5HMZCJcT4EaLQ)中我们开始用01背包来解决问题。
只有确定了如下四点才能把01背包问题套到本题上来。
* 背包的体积为sum / 2
* 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
* 背包如何正好装满,说明找到了总和为 sum / 2 的子集。
* 背包中每一个元素是不可重复放入。
接下来就是一个完整的01背包问题大家应该可以轻松做出了。
## 周四
[动态规划1049. 最后一块石头的重量 II](https://mp.weixin.qq.com/s/WbwAo3jaUaNJjvhHgq0BGg)这道题目其实和[动态规划416. 分割等和子集](https://mp.weixin.qq.com/s/sYw3QtPPQ5HMZCJcT4EaLQ)是非常像的。
本题其实就是尽量让石头分成重量相同的两堆相撞之后剩下的石头最小这样就化解成01背包问题了。
[动态规划416. 分割等和子集](https://mp.weixin.qq.com/s/sYw3QtPPQ5HMZCJcT4EaLQ)相当于是求背包是否正好装满,而本题是求背包最多能装多少。
这两道题目是对dp[target]的处理方式不同。这也考验的对dp[i]定义的理解。
## 总结
总体来说,本周信息量还是比较大的,特别对于对动态规划还不够了解的同学。
但如果坚持下来把,我在文章中列出的每一个问题,都仔细思考,消化为自己的知识,那么进步一定是飞速的。
有的同学可能看了看背包递推公式,上来就能撸它几道题目,然后背包问题就这么过去了,其实这样是很不牢固的。
就像是我们讲解01背包的时候花了那么大力气才把每一个细节都讲清楚这里其实是基础后面的背包问题怎么变基础比较牢固自然会有自己的一套思考过程。
> **相信很多小伙伴刷题的时候面对力扣上近两千道题目感觉无从下手我花费半年时间整理了Github项目「力扣刷题攻略」[https://github.com/youngyangyang04/leetcode-master](https://github.com/youngyangyang04/leetcode-master)。 里面有100多道经典算法题目刷题顺序、配有40w字的详细图解常用算法模板总结以及难点视频讲解按照list一道一道刷就可以了star支持一波吧**
* 公众号:[代码随想录](https://img-blog.csdnimg.cn/20210210152223466.png)
* B站[代码随想录](https://space.bilibili.com/525438321)
* Github[leetcode-master](https://github.com/youngyangyang04/leetcode-master)
* 知乎:[代码随想录](https://www.zhihu.com/people/sun-xiu-yang-64)
![](https://img-blog.csdnimg.cn/20210205113044152.png)

View File

@ -0,0 +1,151 @@
# 本周小结!(动态规划系列四)
## 周一
[动态规划:目标和!](https://mp.weixin.qq.com/s/2pWmaohX75gwxvBENS-NCw)要求在数列之间加入+ 或者 -使其和为S。
所有数的总和为sum假设加法的总和为x那么可以推出x = (S + sum) / 2。
S 和 sum都是固定的那此时问题就转化为01背包问题数列中的数只能使用一次: 给你一些物品数字装满背包就是x有几种方法。
1. 确定dp数组以及下标的含义
**dp[j] 表示填满j包括j这么大容积的包有dp[i]种方法**
2. 确定递推公式
dp[i] += dp[j - nums[j]]
**注意:求装满背包有几种方法类似的题目,递推公式基本都是这样的**
3. dp数组如何初始化
dp[0] 初始化为1 dp[j]其他下标对应的数值应该初始化为0。
4. 确定遍历顺序
01背包问题一维dp的遍历nums放在外循环target在内循环且内循环倒序。
5. 举例推导dp数组
输入nums: [1, 1, 1, 1, 1], S: 3
bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4
dp数组状态变化如下
![494.目标和](https://img-blog.csdnimg.cn/20210125120743274.jpg)
## 周二
这道题目[动态规划:一和零!](https://mp.weixin.qq.com/s/x-u3Dsp76DlYqtCe0xEKJw)算有点难度。
**不少同学都以为是多重背包其实这是一道标准的01背包**
这不过这个背包有两个维度一个是m 一个是n而不同长度的字符串就是不同大小的待装物品。
**所以这是一个二维01背包**
1. 确定dp数组dp table以及下标的含义
**dp[i][j]最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。**
2. 确定递推公式
dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
字符串集合中的一个字符串0的数量为zeroNum1的数量为oneNum。
3. dp数组如何初始化
因为物品价值不会是负数初始为0保证递推的时候dp[i][j]不会被初始值覆盖。
4. 确定遍历顺序
01背包一定是外层for循环遍历物品内层for循环遍历背包容量且从后向前遍历
5. 举例推导dp数组
以输入:["10","0001","111001","1","0"]m = 3n = 3为例
最后dp数组的状态如下所示
![474.一和零](https://img-blog.csdnimg.cn/20210120111201512.jpg)
## 周三
此时01背包我们就讲完了正式开始完全背包。
在[动态规划:关于完全背包,你该了解这些!](https://mp.weixin.qq.com/s/akwyxlJ4TLvKcw26KB9uJw)中我们讲解了完全背包的理论基础。
其实完全背包和01背包区别就是完全背包的物品是无限数量。
递推公式也是一样的,但难点在于遍历顺序上!
完全背包的物品是可以添加多次的,所以遍历背包容量要从小到大去遍历,即:
```C++
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j < bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
```
基本网上题的题解介绍到这里就到此为止了。
**那么为什么要先遍历物品,在遍历背包呢?** (灵魂拷问)
其实对于纯完全背包,先遍历物品,再遍历背包 与 先遍历背包,再遍历物品都是可以的。我在文中[动态规划:关于完全背包,你该了解这些!](https://mp.weixin.qq.com/s/akwyxlJ4TLvKcw26KB9uJw)也给出了详细的解释。
这个细节是很多同学忽略掉的点,其实也不算细节了,**相信不少同学在写背包的时候两层for循环的先后循序搞不清楚靠感觉来的**。
所以理解究竟是先遍历啥,后遍历啥非常重要,这也体现出遍历顺序的重要性!
在文中我也强调了是对纯完全背包两个for循环先后循序无所谓那么题目稍有变化可就有所谓了。
## 周四
在[动态规划:给你一些零钱,你要怎么凑?](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ)中就是给你一堆零钱零钱个数无限为凑成amount的组合数有几种。
**注意这里组合数和排列数的区别!**
看到无限零钱个数就知道是完全背包,
但本题不是纯完全背包了(求是否能装满背包),而是求装满背包有几种方法。
这里在遍历顺序上可就有说法了。
* 如果求组合数就是外层for循环遍历物品内层for遍历背包。
* 如果求排列数就是外层for遍历背包内层for循环遍历物品。
这里同学们需要理解一波,我在文中也给出了详细的解释,下周我们将介绍求排列数的完全背包题目来加深对这个遍历顺序的理解。
## 总结
相信通过本周的学习,大家已经初步感受到遍历顺序的重要性!
很多对动规理解不深入的同学都会感觉动规嘛就是把递推公式推出来其他都easy了。
其实这是一种错觉,或者说对动规理解的不够深入!
我在动规专题开篇介绍[关于动态规划,你该了解这些!](https://mp.weixin.qq.com/s/ocZwfPlCWrJtVGACqFNAag)中就强调了 **递推公式仅仅是 动规五部曲里的一小部分, dp数组的定义、初始化、遍历顺序哪一点没有搞透的话即使知道递推公式遇到稍稍难一点的动规题目立刻会感觉写不出来了**
此时相信大家对动规五部曲也有更深的理解了同样也验证了Carl之前讲过的**简单题是用来学习方法论的,而遇到难题才体现出方法论的重要性!**
> **相信很多小伙伴刷题的时候面对力扣上近两千道题目感觉无从下手我花费半年时间整理了Github项目「力扣刷题攻略」[https://github.com/youngyangyang04/leetcode-master](https://github.com/youngyangyang04/leetcode-master)。 里面有100多道经典算法题目刷题顺序、配有40w字的详细图解常用算法模板总结以及难点视频讲解按照list一道一道刷就可以了star支持一波吧**
* 公众号:[代码随想录](https://img-blog.csdnimg.cn/20210210152223466.png)
* B站[代码随想录](https://space.bilibili.com/525438321)
* Github[leetcode-master](https://github.com/youngyangyang04/leetcode-master)
* 知乎:[代码随想录](https://www.zhihu.com/people/sun-xiu-yang-64)
![](https://img-blog.csdnimg.cn/20210205113044152.png)

View File

@ -0,0 +1,212 @@
# 本周小结!(动态规划系列五)
## 周一
[动态规划377. 组合总和 Ⅳ](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA)中给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数(顺序不同的序列被视作不同的组合)。
题目面试虽然是组合,但又强调顺序不同的序列被视作不同的组合,其实这道题目求的是排列数!
递归公式dp[i] += dp[i - nums[j]];
这个和前上周讲的组合问题又不一样,关键就体现在遍历顺序上!
在[动态规划518.零钱兑换II](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ) 中就已经讲过了。
**如果求组合数就是外层for循环遍历物品内层for遍历背包**
**如果求排列数就是外层for遍历背包内层for循环遍历物品**
如果把遍历nums物品放在外循环遍历target的作为内循环的话举一个例子计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合因为nums遍历放在外层3只能出现在1后面
所以本题遍历顺序最终遍历顺序:**target背包放在外循环将nums物品放在内循环内循环从前到后遍历**。
```C++
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target + 1, 0);
dp[0] = 1;
for (int i = 0; i <= target; i++) { // 遍历背包
for (int j = 0; j < nums.size(); j++) { // 遍历物品
if (i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]]) {
dp[i] += dp[i - nums[j]];
}
}
}
return dp[target];
}
};
```
## 周二
爬楼梯之前我们已经做过了,就是斐波那契数列,很好解,但[动态规划70. 爬楼梯进阶版(完全背包)](https://mp.weixin.qq.com/s/e_wacnELo-2PG76EjrUakA)中我们进阶了一下。
改为:每次可以爬 1 、 2、.....、m 个台阶。问有多少种不同的方法可以爬到楼顶呢?
1阶2阶.... m阶就是物品楼顶就是背包。
每一阶可以重复使用例如跳了1阶还可以继续跳1阶。
问跳到楼顶有几种方法其实就是问装满背包有几种方法。
**此时大家应该发现这就是一个完全背包问题了!**
和昨天的题目[动态规划377. 组合总和 Ⅳ](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA)基本就是一道题了,遍历顺序也是一样一样的!
代码如下:
```C++
class Solution {
public:
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++) { // 遍历物品
if (i - j >= 0) dp[i] += dp[i - j];
}
}
return dp[n];
}
};
```
代码中m表示最多可以爬m个台阶代码中把m改成2就是本题70.爬楼梯可以AC的代码了。
## 周三
[动态规划322.零钱兑换](https://mp.weixin.qq.com/s/dyk-xNilHzNtVdPPLObSeQ)给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数每种硬币的数量是无限的
这里我们都知道这是完全背包。
递归公式dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
关键看遍历顺序。
本题求钱币最小个数,**那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数。**。
所以本题并不强调集合是组合还是排列。
**那么本题的两个for循环的关系是外层for循环遍历物品内层for遍历背包或者外层for遍历背包内层for循环遍历物品都是可以的**
外层for循环遍历物品内层for遍历背包
```C++
// 版本一
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包
if (dp[j - coins[i]] != INT_MAX) { // 如果dp[j - coins[i]]是初始值则跳过
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
}
}
}
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
```
外层for遍历背包内层for循环遍历物品
```C++
// 版本二
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= amount; i++) { // 遍历背包
for (int j = 0; j < coins.size(); j++) { // 遍历物品
if (i - coins[j] >= 0 && dp[i - coins[j]] != INT_MAX ) {
dp[i] = min(dp[i - coins[j]] + 1, dp[i]);
}
}
}
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
}
};
```
## 周四
[动态规划279.完全平方数](https://mp.weixin.qq.com/s/VfJT78p7UGpDZsapKF_QJQ)给定正整数 n找到若干个完全平方数比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少平方数可以重复使用
如果按顺序把前面的文章都看了,这道题目就是简单题了。 dp[i]的定义,递推公式,初始化,遍历顺序,都是和[动态规划322. 零钱兑换](https://mp.weixin.qq.com/s/dyk-xNilHzNtVdPPLObSeQ) 一样一样的。
要是没有前面的基础上来做这道题,那这道题目就有点难度了。
**这也体现了刷题顺序的重要性**。
先遍历背包,在遍历物品:
```C++
// 版本一
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int i = 0; i <= n; i++) { // 遍历背包
for (int j = 1; j * j <= i; j++) { // 遍历物品
dp[i] = min(dp[i - j * j] + 1, dp[i]);
}
}
return dp[n];
}
};
```
先遍历物品,在遍历背包:
```C++
// 版本二
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i * i <= n; i++) { // 遍历物品
for (int j = 1; j <= n; j++) { // 遍历背包
if (j - i * i >= 0) {
dp[j] = min(dp[j - i * i] + 1, dp[j]);
}
}
}
return dp[n];
}
};
```
## 总结
本周的主题其实就是背包问题中的遍历顺序!
我这里做一下总结:
求组合数:[动态规划518.零钱兑换II](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ)
求排列数:[动态规划377. 组合总和 Ⅳ](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA)、[动态规划70. 爬楼梯进阶版(完全背包)](https://mp.weixin.qq.com/s/e_wacnELo-2PG76EjrUakA)
求最小数:[动态规划322. 零钱兑换](https://mp.weixin.qq.com/s/dyk-xNilHzNtVdPPLObSeQ)、[动态规划279.完全平方数](https://mp.weixin.qq.com/s/VfJT78p7UGpDZsapKF_QJQ)
此时我们就已经把完全背包的遍历顺序研究的透透的了!
> **相信很多小伙伴刷题的时候面对力扣上近两千道题目感觉无从下手我花费半年时间整理了Github项目「力扣刷题攻略」[https://github.com/youngyangyang04/leetcode-master](https://github.com/youngyangyang04/leetcode-master)。 里面有100多道经典算法题目刷题顺序、配有40w字的详细图解常用算法模板总结以及难点视频讲解按照list一道一道刷就可以了star支持一波吧**
* 公众号:[代码随想录](https://img-blog.csdnimg.cn/20210210152223466.png)
* B站[代码随想录](https://space.bilibili.com/525438321)
* Github[leetcode-master](https://github.com/youngyangyang04/leetcode-master)
* 知乎:[代码随想录](https://www.zhihu.com/people/sun-xiu-yang-64)
![](https://img-blog.csdnimg.cn/20210205113044152.png)

View File

@ -0,0 +1,302 @@
本周我们主要讲解了打家劫舍系列这个系列也是dp解决的经典问题那么来看看我们收获了哪些呢一起来回顾一下吧。
## 周一
[动态规划:开始打家劫舍!](https://mp.weixin.qq.com/s/UZ31WdLEEFmBegdgLkJ8Dw)中就是给一个数组相邻之间不能连着偷,如果偷才能得到最大金钱。
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://mp.weixin.qq.com/s/kKPx4HpH3RArbRcxAVHbeQ)就是数组成环了,然后相邻的不能连着偷。
这里主要考虑清楚三种情况:
* 情况一:考虑不包含首尾元素
![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://mp.weixin.qq.com/s/UZ31WdLEEFmBegdgLkJ8Dw)是一个逻辑了。
## 周三
[动态规划:还要打家劫舍!](https://mp.weixin.qq.com/s/BOJ1lHsxbQxUZffXlgglEQ)这次是在一颗二叉树上打家劫舍了,条件还是一样的,相临的不能偷。
这道题目是树形DP的入门题目其实树形DP其实就是在树上进行递推公式的推导没有什么神秘的。
这道题目我给出了暴力的解法:
```C++
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把计算过的结果保存一下这样如果计算过孙子了那么计算孩子的时候可以复用孙子节点的结果。
代码如下:
```C++
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. 确定递归函数的参数和返回值
```C++
vector<int> robTree(TreeNode* cur) {
```
dp数组含义下标为0记录不偷该节点所得到的的最大金钱下标为1记录偷该节点所得到的的最大金钱。
**所以本题dp数组就是一个长度为2的数组**
那么有同学可能疑惑长度为2的数组怎么标记树中每个节点的状态呢
**别忘了在递归的过程中,系统栈会保存每一层递归的参数**。
2. 确定终止条件
在遍历的过程中如果遇到空间点的话很明显无论偷还是不偷都是0所以就返回
```
if (cur == NULL) return vector<int>{0, 0};
```
3. 确定遍历顺序
采用后序遍历,代码如下:
```C++
// 下标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}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}
代码如下:
```C++
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://mp.weixin.qq.com/s/kCxlLLjWKaE6nifHC3UL2Q),这也是贪心算法在树上的应用。**那我也可以把这个算法起一个名字,叫做树形贪心**,哈哈哈
“树形贪心”词汇从此诞生,来自「代码随想录」
## 周四
[动态规划:买卖股票的最佳时机](https://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ) 一段时间,只能买买一次,问最大收益。
这里我给出了三中解法:
暴力解法代码:
```
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)
贪心解法代码如下:
因为股票就买卖一次,那么贪心的想法很自然就是取最左最小值,取最右最大值,那么得到的差值就是最大利润。
```C++
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)
动规解法,版本一,代码如下:
```C++
// 版本一
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状态就可以了可以使用滚动数组来节省空间代码如下
```C++
// 版本二
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)
建议先写出版本一,然后在版本一的基础上优化成版本二,而不是直接就写出版本二。
## 总结
刚刚结束了背包问题,本周主要讲解打家劫舍系列。
**劫舍系列简单来说就是 数组上连续元素二选一,成环之后连续元素二选一,在树上连续元素二选一,所能得到的最大价值**
那么这里每一种情况 我在文章中都做了详细的介绍。
周四我们开始讲解股票系列了,大家应该预测到了,我们下周的主题就是股票! 哈哈哈,多么浮躁的一个系列!敬请期待吧!
**代码随想录温馨提醒:投资有风险,入市需谨慎!**

View File

@ -0,0 +1,204 @@
本周的主题就是股票系列,来一起回顾一下吧
## 周一
[动态规划买卖股票的最佳时机II](https://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w)中股票可以买买多了次!
这也是和[121. 买卖股票的最佳时机](https://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ)的唯一区别(注意只有一只股票,所以再次购买前要出售掉之前的股票)
重点在于递推公式公式的不同。
在回顾一下dp数组的含义
* dp[i][0] 表示第i天持有股票所得现金。
* dp[i][1] 表示第i天不持有股票所得最多现金
递推公式:
```
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
```
大家可以发现本题和[121. 买卖股票的最佳时机](https://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ)的代码几乎一样,唯一的区别在:
```
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
```
**这正是因为本题的股票可以买卖多次!** 所以买入股票的时候可能会有之前买卖的利润即dp[i - 1][1]所以dp[i - 1][1] - prices[i]。
## 周二
[动态规划买卖股票的最佳时机III](https://mp.weixin.qq.com/s/Sbs157mlVDtAR0gbLpdKzg)中最多只能完成两笔交易。
**这意味着可以买卖一次,可以买卖两次,也可以不买卖**
1. 确定dp数组以及下标的含义
一天一共就有五个状态,
0. 没有操作
1. 第一次买入
2. 第一次卖出
3. 第二次买入
4. 第二次卖出
**dp[i][j]中 i表示第i天j为 [0 - 4] 五个状态dp[i][j]表示第i天状态j所剩最大现金**
2. 确定递推公式
需要注意dp[i][1]**表示的是第i天买入股票的状态并不是说一定要第i天买入股票这是很多同学容易陷入的误区**。
```
dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);
dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2]);
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
```
3. dp数组如何初始化
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
dp[0][3] = -prices[0];
dp[0][4] = 0;
4. 确定遍历顺序
从递归公式其实已经可以看出一定是从前向后遍历因为dp[i]依靠dp[i - 1]的数值。
5. 举例推导dp数组
以输入[1,2,3,4,5]为例
![123.买卖股票的最佳时机III](https://img-blog.csdnimg.cn/20201228181724295.png)
可以看到红色框为最后两次卖出的状态。
现在最大的时候一定是卖出的状态,而两次卖出的状态现金最大一定是最后一次卖出。
所以最终最大利润是dp[4][4]
## 周三
[动态规划买卖股票的最佳时机IV](https://mp.weixin.qq.com/s/jtxZJWAo2y5sUsW647Z5cw)最多可以完成 k 笔交易。
相对于上一道[动态规划123.买卖股票的最佳时机III](https://mp.weixin.qq.com/s/Sbs157mlVDtAR0gbLpdKzg)本题需要通过前两次的交易来类比前k次的交易
1. 确定dp数组以及下标的含义
使用二维数组 dp[i][j] 第i天的状态为j所剩下的最大现金是dp[i][j]
j的状态表示为
* 0 表示不操作
* 1 第一次买入
* 2 第一次卖出
* 3 第二次买入
* 4 第二次卖出
* .....
**除了0以外偶数就是卖出奇数就是买入**
2. 确定递推公式
还要强调一下dp[i][1]**表示的是第i天买入股票的状态并不是说一定要第i天买入股票这是很多同学容易陷入的误区**。
```C++
for (int j = 0; j < 2 * k - 1; j += 2) {
dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
}
```
**本题和[动态规划123.买卖股票的最佳时机III](https://mp.weixin.qq.com/s/Sbs157mlVDtAR0gbLpdKzg)最大的区别就是这里要类比j为奇数是买偶数是卖剩的状态**。
3. dp数组如何初始化
**dp[0][j]当j为奇数的时候都初始化为 -prices[0]**
代码如下:
```C++
for (int j = 1; j < 2 * k; j += 2) {
dp[0][j] = -prices[0];
}
```
**在初始化的地方同样要类比j为偶数是买、奇数是卖的状态**。
4. 确定遍历顺序
从递归公式其实已经可以看出一定是从前向后遍历因为dp[i]依靠dp[i - 1]的数值。
5. 举例推导dp数组
以输入[1,2,3,4,5]k=2为例。
![188.买卖股票的最佳时机IV](https://img-blog.csdnimg.cn/20201229100358221.png)
最后一次卖出一定是利润最大的dp[prices.size() - 1][2 * k]即红色部分就是最后求解。
## 周四
[动态规划:最佳买卖股票时机含冷冻期](https://mp.weixin.qq.com/s/IgC0iWWCDpYL9ZbTHGHgfw)尽可能地完成更多的交易多次买卖一支股票但有冷冻期冷冻期为1天
相对于[动态规划122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w),本题加上了一个冷冻期
**本题则需要第三个状态:不持有股票(冷冻期)的最多现金**。
动规五部曲,分析如下:
1. 确定dp数组以及下标的含义
**dp[i][j]第i天状态为j所剩的最多现金为dp[i][j]**。
j的状态为
* 1持有股票后的最多现金
* 2不持有股票能购买的最多现金
* 3不持有股票冷冻期的最多现金
2. 确定递推公式
```
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][2]);
dp[i][2] = dp[i - 1][0] + prices[i];
```
3. dp数组如何初始化
可以统一都初始为0了。
代码如下:
```
vector<vector<int>> dp(n, vector<int>(3, 0));
```
**初始化其实很有讲究很多同学可能是稀里糊涂的全都初始化0反正就可以通过但没有想清楚为什么都初始化为0**
4. 确定遍历顺序
从递归公式上可以看出dp[i] 依赖于 dp[i-1],所以是从前向后遍历。
5. 举例推导dp数组
以 [1,2,3,0,2] 为例dp数组如下
![309.最佳买卖股票时机含冷冻期](https://img-blog.csdnimg.cn/20201229163725348.png)
最后两个状态 不持有股票(能购买) 和 不持有股票(冷冻期)都有可能最后结果,取最大的。
## 总结
下周还会有一篇股票系列的文章,**股票系列后面我也会单独写一篇总结,来高度概括一下,这样大家会对股票问题就有一个整体性的理解了**。