动态规划4
198.打家劫舍
分析:偷相邻的会报警,所以问题就是如何在不偷相邻的房间然后达到最大。
仔细一想,当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了。这个问题很容易感觉到,当前状态和前面状态会有一种依赖关系,那么这种依赖关系都是动规的递推公式。
- 确定dp数组(dp table)以及下标的含义
**dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]**。
- 确定递推公式
如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,即:第i-1房一定是不考虑的,找出 下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。
如果不偷第i房间,那么dp[i] = dp[i - 1],即考 虑i-1房,(注意这里是考虑,并不是一定要偷i-1房,这是很多同学容易混淆的点)
决定dp[i]的因素就是第i房间偷还是不偷。
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
- dp数组如何初始化
递推公式的基础就是dp[0] 和 dp[1]从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);
- 确定遍历顺序
dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历!
- 举例推导dp数组
以示例二,输入[2,7,9,3,1]为例。
代码
1 | class Solution { |
213.打家劫舍II
现在问题进化房间组成一个环,也就是最后一个和第一个是相邻的房间
所以我们要画出问题的状态,并且尽量的屡成一条线
对于一个数组,成环的话主要有如下三种情况:
- 情况一:考虑不包含首尾元素
- 情况二:考虑包含首元素,不包含尾元素
- 情况三:考虑包含尾元素,不包含首元素
而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了。
因为假设现在是情况2 但是下标对应的值num[0]<[1]就会自动转换成情况一,所以最后就是考虑两种情况。
1 | // 注意注释中的情况二情况三,以及把198.打家劫舍的代码抽离出来了 |
337.打家劫舍 III
家庭呈现树形排列,只能偷父与子中的一个
先是一个优化的回溯方法
1 | class Solution { |
然后就是以树形dp的方式来解决这个题目
这个题目算是树形DP的入门题目
- 确定递归函数的参数和返回值
这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。
dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
为什么是长度为2呢因为只用判断父与子之间的关系。
- 确定终止条件
在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回
1 | if (cur == NULL) return vector<int>{0, 0}; |
- 确定遍历顺序
首先明确的是使用后序遍历。 因为要通过递归函数的返回值来做下一步计算。因为我们需要判断左右节点与根节点的关系,如果是后序遍历的话,判断的过程就放在了
通过递归左节点,得到左节点偷与不偷的金钱。
通过递归右节点,得到右节点偷与不偷的金钱。
- 确定单层递归的逻辑
如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; (下标为0记录的是不偷获取的最大值)
如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);
最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}
代码如下:
1 | vector<int> left = robTree(cur->left); // 左 |
1 | class Solution { |
所以树形DO只不过平时我们习惯了在一维数组或者二维数组上推导公式,一下子换成了树,就需要对树的遍历方式足够了解!
121. 买卖股票的最佳时机
买股票的题用贪心做也可以,用DP做也可以。
贪心算法
1 | class Solution { |
在寻找low值的同时就计算了result;
动态规划
动规五部曲分析如下:
- 确定dp数组(dp table)以及下标的含义
动态规划中最难想的就是DP数组的意义,买卖股票的题我们设置一个二维的DP数组其中
dp[i][0] 表示第i天持有股票所得最多现金
注意这里说的是“持有”,“持有”不代表就是当天“买入”!也有可能是昨天就买入了,今天保持持有的状态,对于一次买卖来说持有就代表着,当前最低的买入价格。而每天的持有又依赖于上一天的持有,对于能明确这种依赖关系的最好就使用DP实现。
dp[i][1] 表示第i天不持有股票所得最多现金
可以由两个状态推出来
- 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
- 第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金即:prices[i] + dp[i - 1][0]
所以递推公式是:dp[i][0] = max(dp[i - 1][0], -prices[i]);
dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
- dp数组如何初始化
dp[0][0]表示第0天持有股票,此时的持有股票就一定是买入股票了,因为不可能有前一天推出来,所以dp[0][0] -= prices[0];这个完全是为了可以满足递推关系所写,没有实际的意义。
dp[0][1]表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0][1] = 0;
- 确定遍历顺序
从递推公式可以看出dp[i]都是由dp[i - 1]推导出来的,那么一定是从前向后遍历。
1 | class Solution { |
122.买卖股票的最佳时机II
可以进行多次买卖,但是不能同时参与多笔交易
那么我每次卖了之后我持有的资金就会变化,所以不同主要体现在DP公式上dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
1 | class Solution { |
123.买卖股票的最佳时机III
最多可以完成两笔交易,和前边的可以进行多次交易形成对比,多次交易持有的资金是上一次的卖出资金,那么最多进行两次交易的第二次交易是在第一次卖出的基础上,更新持有的资金。
接来下我用动态规划五部曲详细分析一下:
- 确定dp数组以及下标的含义
一天一共就有五个状态,
- 没有操作 (其实我们也可以不设置这个状态)
- 第一次持有股票
- 第一次不持有股票
- 第二次持有股票
- 第二次不持有股票
dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。
需要注意:dp[i][1],表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票。
例如 dp[i][1] ,并不是说 第i天一定买入股票,有可能 第 i-1天 就买入了,那么 dp[i][1] 延续买入股票的这个状态。
- 确定递推公式
达到dp[i][1]状态,有两个具体操作:
- 操作一:第i天买入股票了,那么dp[i][1] = dp[i-1][0] - prices[i]
- 操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1]
这两个操作中一定是选最大的,所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);
同理dp[i][2]也有两个操作:
- 操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
- 操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2]
所以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]);
- dp数组如何初始化
第0天做第一次买入的操作,dp[0][1] = -prices[0];
第0天做第一次卖出的操作,这个初始值应该是多少呢?
此时还没有买入,怎么就卖出呢? 其实大家可以理解当天买入,当天卖出,所以dp[0][2] = 0;
第0天第二次买入操作,初始值应该是多少呢?应该不少同学疑惑,第一次还没买入呢,怎么初始化第二次买入呢?
第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后再买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。
所以第二次买入操作,初始化为:dp[0][3] = -prices[0];
同理第二次卖出初始化dp[0][4] = 0;
- 确定遍历顺序
从递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i],依靠dp[i - 1]的数值。
- 举例推导dp数组
以输入[1,2,3,4,5]为例

1 | class Solution { |
188.买卖股票的最佳时机IV
hard难度题,最多可以完成 k
笔交易,和上一题的分析类似。
1 | class Solution { |
309.最佳买卖股票时机含冷冻期
是因为出现冷冻期之后,状态其实是比较复杂度,例如今天买入股票、今天卖出股票、今天是冷冻期,都是不能操作股票的。
但是我们可以区分出如下四个状态:
- 状态一:持有股票状态(今天买入股票,或者是之前就买入了股票然后没有操作,一直持有)
- 不持有股票状态,这里就有两种卖出股票状态
- 状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,一直没操作)
- 状态三:今天卖出股票
- 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!
根绝这个图我们可以看出转移到持有股票状态现在有两种方式,一种还是之前的卖出状态,还多了一个冷冻状态,代表买入的前一天是冷冻状态。
- 确定递推公式
达到买入股票状态(状态一)即:dp[i][0],有两个具体操作:
- 操作一:前一天就是持有股票状态(状态一),dp[i][0] = dp[i - 1][0]
- 操作二:今天买入了,有两种情况
- 前一天是冷冻期(状态四),dp[i - 1][3] - prices[i]
- 前一天是保持卖出股票的状态(状态二),dp[i - 1][1] - prices[i]
那么dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]);
达到保持卖出股票状态(状态二)即:dp[i][1],有两个具体操作:
- 操作一:前一天就是状态二
- 操作二:前一天是冷冻期(状态四)
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
达到今天就卖出股票状态(状态三),即:dp[i][2] ,只有一个操作:
昨天一定是持有股票状态(状态一),今天卖出
即:dp[i][2] = dp[i - 1][0] + prices[i];
达到冷冻期状态(状态四),即:dp[i][3],只有一个操作:
昨天卖出了股票(状态三)
dp[i][3] = dp[i - 1][2];
综上分析,递推代码如下:
1 | dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]); |
经过这样的拆分,就可以把冷冻区融进了DP数组
- dp数组如何初始化
这里主要讨论一下第0天如何初始化。
如果是持有股票状态(状态一)那么:dp[0][0] = -prices[0],一定是当天买入股票。
保持卖出股票状态(状态二),这里其实从 「状态二」的定义来说 ,很难明确应该初始多少,这种情况我们就看递推公式需要我们给他初始成什么数值。
如果i为1,第1天买入股票,那么递归公式中需要计算 dp[i - 1][1] - prices[i] ,即 dp[0][1] - prices[1],那么大家感受一下 dp[0][1] (即第0天的状态二)应该初始成多少,只能初始为0。想一想如果初始为其他数值,是我们第1天买入股票后 手里还剩的现金数量是不是就不对了。
今天卖出了股票(状态三),同上分析,dp[0][2]初始化为0,dp[0][3]也初始为0。
1 | class Solution { |
714.买卖股票的最佳时机含手续费
1 | class Solution { |
主要的不同体现在返回值可能不买才使最优的操作,所以要在dp[n - 1][0], dp[n - 1][1]选一个最大的。