mirror of
https://github.com/youngyangyang04/leetcode-master.git
synced 2025-07-07 07:35:35 +08:00
Merge pull request #1831 from juguagua/leetcode-modify-the-code-of-the-dp
更新动态规划部分:从 “单词拆分” 到 “买卖股票的最佳时机III”
This commit is contained in:
@ -89,7 +89,7 @@ dp[i][1] 表示第i天不持有股票所得最多现金
|
||||
|
||||
**注意这里说的是“持有”,“持有”不代表就是当天“买入”!也有可能是昨天就买入了,今天保持持有的状态**
|
||||
|
||||
很多同学把“持有”和“买入”没分区分清楚。
|
||||
很多同学把“持有”和“买入”没区分清楚。
|
||||
|
||||
在下面递推公式分析中,我会进一步讲解。
|
||||
|
||||
@ -103,11 +103,11 @@ dp[i][1] 表示第i天不持有股票所得最多现金
|
||||
|
||||
如果第i天不持有股票即dp[i][1], 也可以由两个状态推出来
|
||||
* 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
|
||||
* 第i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0]
|
||||
* 第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金即:prices[i] + dp[i - 1][0]
|
||||
|
||||
同样dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
|
||||
|
||||
这样递归公式我们就分析完了
|
||||
这样递推公式我们就分析完了
|
||||
|
||||
3. dp数组如何初始化
|
||||
|
||||
@ -121,7 +121,7 @@ dp[0][1]表示第0天不持有股票,不持有股票那么现金就是0,所
|
||||
|
||||
4. 确定遍历顺序
|
||||
|
||||
从递推公式可以看出dp[i]都是有dp[i - 1]推导出来的,那么一定是从前向后遍历。
|
||||
从递推公式可以看出dp[i]都是由dp[i - 1]推导出来的,那么一定是从前向后遍历。
|
||||
|
||||
5. 举例推导dp数组
|
||||
|
||||
@ -326,29 +326,17 @@ Go:
|
||||
> 贪心法:
|
||||
```Go
|
||||
func maxProfit(prices []int) int {
|
||||
low := math.MaxInt32
|
||||
rlt := 0
|
||||
for i := range prices{
|
||||
low = min(low, prices[i])
|
||||
rlt = max(rlt, prices[i]-low)
|
||||
min := prices[0]
|
||||
res := 0
|
||||
for i := 1; i < len(prices); i++ {
|
||||
if prices[i] - min > res {
|
||||
res = prices[i]-min
|
||||
}
|
||||
|
||||
return rlt
|
||||
if min > prices[i] {
|
||||
min = prices[i]
|
||||
}
|
||||
func min(a, b int) int {
|
||||
if a < b{
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b{
|
||||
return a
|
||||
}
|
||||
|
||||
return b
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
@ -361,7 +349,6 @@ func maxProfit(prices []int) int {
|
||||
for i := 0; i < length; i++ {
|
||||
dp[i] = make([]int, 2)
|
||||
}
|
||||
|
||||
dp[0][0] = -prices[0]
|
||||
dp[0][1] = 0
|
||||
for i := 1; i < length; i++ {
|
||||
|
@ -39,7 +39,7 @@
|
||||
本题我们在讲解贪心专题的时候就已经讲解过了[贪心算法:买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II.html),只不过没有深入讲解动态规划的解法,那么这次我们再好好分析一下动规的解法。
|
||||
|
||||
|
||||
本题和[121. 买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)的唯一区别本题股票可以买卖多次了(注意只有一只股票,所以再次购买前要出售掉之前的股票)
|
||||
本题和[121. 买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)的唯一区别是本题股票可以买卖多次了(注意只有一只股票,所以再次购买前要出售掉之前的股票)
|
||||
|
||||
**在动规五部曲中,这个区别主要是体现在递推公式上,其他都和[121. 买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)一样一样的**。
|
||||
|
||||
@ -63,9 +63,9 @@
|
||||
|
||||
那么第i天持有股票即dp[i][0],如果是第i天买入股票,所得现金就是昨天不持有股票的所得现金 减去 今天的股票价格 即:dp[i - 1][1] - prices[i]。
|
||||
|
||||
在来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来
|
||||
再来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来
|
||||
* 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
|
||||
* 第i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0]
|
||||
* 第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金即:prices[i] + dp[i - 1][0]
|
||||
|
||||
**注意这里和[121. 买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)就是一样的逻辑,卖出股票收获利润(可能是负值)天经地义!**
|
||||
|
||||
@ -99,7 +99,7 @@ dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
|
||||
|
||||
**这正是因为本题的股票可以买卖多次!** 所以买入股票的时候,可能会有之前买卖的利润即:dp[i - 1][1],所以dp[i - 1][1] - prices[i]。
|
||||
|
||||
想到到这一点,对这两道题理解的比较深刻了。
|
||||
想到到这一点,对这两道题理解的就比较深刻了。
|
||||
|
||||
这里我依然给出滚动数组的版本,C++代码如下:
|
||||
|
||||
@ -228,29 +228,6 @@ func max(a, b int) int {
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
func maxProfit(prices []int) int {
|
||||
//创建数组
|
||||
dp:=make([][]int,len(prices))
|
||||
for i:=0;i<len(prices);i++{
|
||||
dp[i]=make([]int,2)
|
||||
}
|
||||
dp[0][0]=-prices[0]
|
||||
dp[0][1]=0
|
||||
for i:=1;i<len(prices);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])
|
||||
}
|
||||
return dp[len(prices)-1][1]
|
||||
}
|
||||
func max(a,b int)int{
|
||||
if a<b{
|
||||
return b
|
||||
}
|
||||
return a
|
||||
}
|
||||
```
|
||||
|
||||
Javascript:
|
||||
```javascript
|
||||
// 方法一:动态规划(dp 数组)
|
||||
|
@ -62,7 +62,7 @@ dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天
|
||||
|
||||
需要注意:dp[i][1],**表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区**。
|
||||
|
||||
例如 dp[i][1] ,并不是说 第i点一定买入股票,有可能 第 i-1天 就买入了,那么 dp[i][1] 延续买入股票的这个状态。
|
||||
例如 dp[i][1] ,并不是说 第i天一定买入股票,有可能 第 i-1天 就买入了,那么 dp[i][1] 延续买入股票的这个状态。
|
||||
|
||||
2. 确定递推公式
|
||||
|
||||
@ -102,7 +102,7 @@ dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
|
||||
|
||||
第0天第二次买入操作,初始值应该是多少呢?应该不少同学疑惑,第一次还没买入呢,怎么初始化第二次买入呢?
|
||||
|
||||
第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后在买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。
|
||||
第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后再买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。
|
||||
|
||||
所以第二次买入操作,初始化为:dp[0][3] = -prices[0];
|
||||
|
||||
@ -181,7 +181,7 @@ public:
|
||||
|
||||
dp[1] = max(dp[1], dp[0] - prices[i]); 如果dp[1]取dp[1],即保持买入股票的状态,那么 dp[2] = max(dp[2], dp[1] + prices[i]);中dp[1] + prices[i] 就是今天卖出。
|
||||
|
||||
如果dp[1]取dp[0] - prices[i],今天买入股票,那么dp[2] = max(dp[2], dp[1] + prices[i]);中的dp[1] + prices[i]相当于是尽在再卖出股票,一买一卖收益为0,对所得现金没有影响。相当于今天买入股票又卖出股票,等于没有操作,保持昨天卖出股票的状态了。
|
||||
如果dp[1]取dp[0] - prices[i],今天买入股票,那么dp[2] = max(dp[2], dp[1] + prices[i]);中的dp[1] + prices[i]相当于是今天再卖出股票,一买一卖收益为0,对所得现金没有影响。相当于今天买入股票又卖出股票,等于没有操作,保持昨天卖出股票的状态了。
|
||||
|
||||
**这种写法看上去简单,其实思路很绕,不建议大家这么写,这么思考,很容易把自己绕进去!**
|
||||
|
||||
@ -407,39 +407,6 @@ function maxProfit(prices: number[]): number {
|
||||
};
|
||||
```
|
||||
|
||||
Go:
|
||||
|
||||
> 版本一:
|
||||
```go
|
||||
// 买卖股票的最佳时机III 动态规划
|
||||
// 时间复杂度O(n) 空间复杂度O(n)
|
||||
func maxProfit(prices []int) int {
|
||||
dp := make([][]int, len(prices))
|
||||
status := make([]int, len(prices) * 4)
|
||||
for i := range dp {
|
||||
dp[i] = status[:4]
|
||||
status = status[4:]
|
||||
}
|
||||
dp[0][0], dp[0][2] = -prices[0], -prices[0]
|
||||
|
||||
for i := 1; i < len(prices); i++ {
|
||||
dp[i][0] = max(dp[i - 1][0], -prices[i])
|
||||
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i])
|
||||
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] - prices[i])
|
||||
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] + prices[i])
|
||||
}
|
||||
|
||||
return dp[len(prices) - 1][3]
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -138,7 +138,7 @@ public:
|
||||
|
||||
3. dp数组如何初始化
|
||||
|
||||
从递归公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递归的根基,dp[0]一定要为true,否则递归下去后面都都是false了。
|
||||
从递推公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递推的根基,dp[0]一定要为true,否则递推下去后面都都是false了。
|
||||
|
||||
那么dp[0]有没有意义呢?
|
||||
|
||||
@ -152,13 +152,13 @@ dp[0]表示如果字符串为空的话,说明出现在字典里。
|
||||
|
||||
题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。
|
||||
|
||||
还要讨论两层for循环的前后循序。
|
||||
还要讨论两层for循环的前后顺序。
|
||||
|
||||
**如果求组合数就是外层for循环遍历物品,内层for遍历背包**。
|
||||
|
||||
**如果求排列数就是外层for遍历背包,内层for循环遍历物品**。
|
||||
|
||||
我在这里做一个一个总结:
|
||||
我在这里做一个总结:
|
||||
|
||||
求组合数:[动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html)
|
||||
求排列数:[动态规划:377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和.html)、[动态规划:70. 爬楼梯进阶版(完全背包)](https://programmercarl.com/0070.爬楼梯完全背包版本.html)
|
||||
@ -170,7 +170,7 @@ dp[0]表示如果字符串为空的话,说明出现在字典里。
|
||||
|
||||
"apple" + "apple" + "pen" 或者 "pen" + "apple" + "apple" 是不可以的,那么我们就是强调物品之间顺序。
|
||||
|
||||
所以说,本题一定是 先遍历 背包,在遍历物品。
|
||||
所以说,本题一定是 先遍历 背包,再遍历物品。
|
||||
|
||||
5. 举例推导dp[i]
|
||||
|
||||
@ -209,7 +209,7 @@ public:
|
||||
|
||||
关于遍历顺序,再给大家讲一下为什么 先遍历物品再遍历背包不行。
|
||||
|
||||
这里可以给出先遍历物品在遍历背包的代码:
|
||||
这里可以给出先遍历物品再遍历背包的代码:
|
||||
|
||||
```CPP
|
||||
class Solution {
|
||||
@ -241,7 +241,7 @@ public:
|
||||
|
||||
最后dp[s.size()] = 0 即 dp[13] = 0 ,而不是1,因为先用 "apple" 去遍历的时候,dp[8]并没有被赋值为1 (还没用"pen"),所以 dp[13]也不能变成1。
|
||||
|
||||
除非是先用 "apple" 遍历一遍,在用 "pen" 遍历,此时 dp[8]已经是1,最后再用 "apple" 去遍历,dp[13]才能是1。
|
||||
除非是先用 "apple" 遍历一遍,再用 "pen" 遍历,此时 dp[8]已经是1,最后再用 "apple" 去遍历,dp[13]才能是1。
|
||||
|
||||
如果大家对这里不理解,建议可以把我上面给的代码,拿去力扣上跑一跑,把dp数组打印出来,对着递推公式一步一步去看,思路就清晰了。
|
||||
|
||||
|
@ -154,22 +154,13 @@ class Solution:
|
||||
Go:
|
||||
```Go
|
||||
func rob(nums []int) int {
|
||||
if len(nums)<1{
|
||||
return 0
|
||||
n := len(nums)
|
||||
dp := make([]int, n+1) // dp[i]表示偷到第i家能够偷得的最大金额
|
||||
dp[1] = nums[0]
|
||||
for i := 2; i <= n; i++ {
|
||||
dp[i] = max(dp[i-1], dp[i-2] + nums[i-1])
|
||||
}
|
||||
if len(nums)==1{
|
||||
return nums[0]
|
||||
}
|
||||
if len(nums)==2{
|
||||
return max(nums[0],nums[1])
|
||||
}
|
||||
dp :=make([]int,len(nums))
|
||||
dp[0]=nums[0]
|
||||
dp[1]=max(nums[0],nums[1])
|
||||
for i:=2;i<len(nums);i++{
|
||||
dp[i]=max(dp[i-2]+nums[i],dp[i-1])
|
||||
}
|
||||
return dp[len(dp)-1]
|
||||
return dp[n]
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
|
@ -143,6 +143,44 @@ class Solution:
|
||||
return dp[-1]
|
||||
```
|
||||
|
||||
Go:
|
||||
|
||||
```go
|
||||
// 打家劫舍Ⅱ 动态规划
|
||||
// 时间复杂度O(n) 空间复杂度O(n)
|
||||
func rob(nums []int) int {
|
||||
if len(nums) == 1 {
|
||||
return nums[0]
|
||||
}
|
||||
if len(nums) == 2 {
|
||||
return max(nums[0], nums[1])
|
||||
}
|
||||
|
||||
result1 := robRange(nums, 0)
|
||||
result2 := robRange(nums, 1)
|
||||
return max(result1, result2)
|
||||
}
|
||||
|
||||
// 偷盗指定的范围
|
||||
func robRange(nums []int, start int) int {
|
||||
dp := make([]int, len(nums))
|
||||
dp[1] = nums[start]
|
||||
|
||||
for i := 2; i < len(nums); i++ {
|
||||
dp[i] = max(dp[i - 2] + nums[i - 1 + start], dp[i - 1])
|
||||
}
|
||||
|
||||
return dp[len(nums) - 1]
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
javascipt:
|
||||
```javascript
|
||||
var rob = function(nums) {
|
||||
@ -187,44 +225,6 @@ function robRange(nums: number[], start: number, end: number): number {
|
||||
}
|
||||
```
|
||||
|
||||
Go:
|
||||
|
||||
```go
|
||||
// 打家劫舍Ⅱ 动态规划
|
||||
// 时间复杂度O(n) 空间复杂度O(n)
|
||||
func rob(nums []int) int {
|
||||
if len(nums) == 1 {
|
||||
return nums[0]
|
||||
}
|
||||
if len(nums) == 2 {
|
||||
return max(nums[0], nums[1])
|
||||
}
|
||||
|
||||
result1 := robRange(nums, 0)
|
||||
result2 := robRange(nums, 1)
|
||||
return max(result1, result2)
|
||||
}
|
||||
|
||||
// 偷盗指定的范围
|
||||
func robRange(nums []int, start int) int {
|
||||
dp := make([]int, len(nums))
|
||||
dp[1] = nums[start]
|
||||
|
||||
for i := 2; i < len(nums); i++ {
|
||||
dp[i] = max(dp[i - 2] + nums[i - 1 + start], dp[i - 1])
|
||||
}
|
||||
|
||||
return dp[len(nums) - 1]
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
<p align="center">
|
||||
|
@ -129,7 +129,7 @@ if (cur == NULL) return vector<int>{0, 0};
|
||||
|
||||
3. 确定遍历顺序
|
||||
|
||||
首先明确的是使用后序遍历。 因为通过递归函数的返回值来做下一步计算。
|
||||
首先明确的是使用后序遍历。 因为要通过递归函数的返回值来做下一步计算。
|
||||
|
||||
通过递归左节点,得到左节点偷与不偷的金钱。
|
||||
|
||||
@ -147,7 +147,7 @@ vector<int> right = robTree(cur->right); // 右
|
||||
|
||||
4. 确定单层递归的逻辑
|
||||
|
||||
如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; (**如果对下标含义不理解就在回顾一下dp数组的含义**)
|
||||
如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; (**如果对下标含义不理解就再回顾一下dp数组的含义**)
|
||||
|
||||
如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);
|
||||
|
||||
@ -483,37 +483,6 @@ function robNode(node: TreeNode | null): MaxValueArr {
|
||||
}
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```go
|
||||
// 打家劫舍Ⅲ 动态规划
|
||||
// 时间复杂度O(n) 空间复杂度O(logn)
|
||||
func rob(root *TreeNode) int {
|
||||
dp := traversal(root)
|
||||
return max(dp[0], dp[1])
|
||||
}
|
||||
|
||||
func traversal(cur *TreeNode) []int {
|
||||
if cur == nil {
|
||||
return []int{0, 0}
|
||||
}
|
||||
|
||||
dpL := traversal(cur.Left)
|
||||
dpR := traversal(cur.Right)
|
||||
|
||||
val1 := cur.Val + dpL[0] + dpR[0] // 偷盗当前节点
|
||||
val2 := max(dpL[0], dpL[1]) + max(dpR[0], dpR[1]) // 不偷盗当前节点
|
||||
return []int{val2, val1}
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
<p align="center">
|
||||
|
Reference in New Issue
Block a user