本节内容
309.最佳买卖股票时机含冷冻期
714.买卖股票的最佳时机含手续费
股票问题总结
309.最佳买卖股票时机含冷冻期※ 建议 :
题目链接: https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/ 文章讲解: https://programmercarl.com/0309.%E6%9C%80%E4%BD%B3%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E6%97%B6%E6%9C%BA%E5%90%AB%E5%86%B7%E5%86%BB%E6%9C%9F.html 视频讲解:
题目分析
方案一 dp算法总是给我一种不真实的感觉,写完之后,代码通过了,才知道这是思路是对的。在没有AC之前,我也不确定自己的方法对不对,没有太大信心。似乎理解了,又似乎不理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public int maxProfit (int [] prices) { int [][] dp = new int [prices.length][3 ]; dp[0 ][0 ] = -prices[0 ]; for (int i = 1 ; i < prices.length; i++) { dp[i][0 ] = Math.max(dp[i - 1 ][0 ], dp[i - 1 ][2 ] - prices[i]); dp[i][1 ] = Math.max(dp[i - 1 ][1 ], dp[i - 1 ][0 ] + prices[i]); dp[i][2 ] = Math.max(dp[i - 1 ][2 ], dp[i - 1 ][1 ]); } return Math.max(dp[prices.length - 1 ][1 ], dp[prices.length - 1 ][2 ]); } }
结果 解答成功: 执行耗时:1 ms,击败了73.34% 的Java用户 内存消耗:39.8 MB,击败了13.91% 的Java用户
分析 时间复杂度: O( n )
空间复杂度: O( n )
代码随想录 https://programmercarl.com/0309.%E6%9C%80%E4%BD%B3%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E6%97%B6%E6%9C%BA%E5%90%AB%E5%86%B7%E5%86%BB%E6%9C%9F.html
思路 相对于 122.买卖股票的最佳时机II ,本题加上了一个冷冻期
在 122.买卖股票的最佳时机II 中有两个状态,持有股票后的最多现金,和不持有股票的最多现金。
动规五部曲,分析如下:
确定dp数组以及下标的含义
dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]。
其实本题很多同学搞的比较懵,是因为出现冷冻期之后,状态其实是比较复杂度 ,例如今天买入股票、今天卖出股票、今天是冷冻期,都是不能操作股票的。
具体可以区分出如下四个状态:
状态一:持有股票状态(今天买入股票,或者是之前就买入了股票然后没有操作,一直持有)
不持有股票状态,这里就有两种卖出股票状态
状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,一直没操作)
状态三:今天卖出股票
状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!
j的状态为:
0:状态一:持有股票状态
1:状态二:保持卖出股票的状态
2:状态三:今天卖出股票
3:状态四:今天为冷冻期状态
很多题解为什么讲的比较模糊,是因为把这四个状态合并成三个状态了,其实就是把状态二和状态四合并在一起了。
从代码上来看确实可以合并,但从逻辑上分析合并之后就很难理解了,所以我下面的讲解是按照这四个状态来的,把每一个状态分析清楚。
如果大家按照代码随想录顺序来刷的话,会发现 买卖股票最佳时机 1,2,3,4 的题目讲解中
买卖股票的最佳时机
122.买卖股票的最佳时机II
123.买卖股票的最佳时机III
188.买卖股票的最佳时机IV
「今天卖出股票」我是没有单独列出一个状态的归类为「不持有股票的状态」,而本题为什么要单独列出「今天卖出股票」 一个状态呢?
因为本题我们有冷冻期,而冷冻期的前一天,只能是 「今天卖出股票」状态,如果是 「不持有股票状态」那么就很模糊,因为不一定是 卖出股票的操作。
如果没有按照 代码随想录 顺序去刷的录友,可能看这里的讲解 会有点困惑,建议把代码随想录本篇之前股票内容的讲解都看一下,领会一下每天 状态的设置。
注意这里的每一个状态,例如状态一,是持有股票股票状态并不是说今天一定就买入股票,而是说保持买入股票的状态即:可能是前几天买入的,之后一直没操作,所以保持买入股票的状态 。
确定递推公式
达到买入股票状态 (状态一)即: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 2 3 4 dp[i][0 ] = max (dp[i - 1 ][0 ], max (dp[i - 1 ][3 ], dp[i - 1 ][1 ]) - prices[i]); dp[i][1 ] = max (dp[i - 1 ][1 ], dp[i - 1 ][3 ]); dp[i][2 ] = dp[i - 1 ][0 ] + prices[i]; dp[i][3 ] = dp[i - 1 ][2 ];
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。
确定遍历顺序
从递归公式上可以看出,dp[i] 依赖于 dp[i-1],所以是从前向后遍历。
举例推导dp数组
以 [1,2,3,0,2] 为例,dp数组如下:
最后结果是取 状态二,状态三,和状态四的最大值,不少同学会把状态四忘了,状态四是冷冻期,最后一天如果是冷冻期也可能是最大值。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution {public : int maxProfit (vector<int >& prices) { int n = prices.size (); if (n == 0 ) return 0 ; vector<vector<int >> dp (n, vector <int >(4 , 0 )); dp[0 ][0 ] -= prices[0 ]; for (int i = 1 ; i < n; i++) { dp[i][0 ] = max (dp[i - 1 ][0 ], max (dp[i - 1 ][3 ] - prices[i], dp[i - 1 ][1 ] - prices[i])); dp[i][1 ] = max (dp[i - 1 ][1 ], dp[i - 1 ][3 ]); dp[i][2 ] = dp[i - 1 ][0 ] + prices[i]; dp[i][3 ] = dp[i - 1 ][2 ]; } return max (dp[n - 1 ][3 ], max (dp[n - 1 ][1 ], dp[n - 1 ][2 ])); } };
当然,空间复杂度可以优化,定义一个dp[2][4]大小的数组就可以了,就保存前一天的当前的状态,感兴趣的同学可以自己去写一写,思路是一样的。
总结
这次把冷冻期这道题目,讲的很透彻了,细分为四个状态,其状态转移也十分清晰,建议大家都按照四个状态来分析,如果只划分三个状态确实很容易给自己绕进去。
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution { public int maxProfit (int [] prices) { if (prices == null || prices.length < 2 ) { return 0 ; } int [][] dp = new int [prices.length][2 ]; dp[0 ][0 ] = 0 ; dp[0 ][1 ] = -prices[0 ]; dp[1 ][0 ] = Math.max(dp[0 ][0 ], dp[0 ][1 ] + prices[1 ]); dp[1 ][1 ] = Math.max(dp[0 ][1 ], -prices[1 ]); for (int i = 2 ; i < prices.length; i++) { dp[i][0 ] = Math.max(dp[i - 1 ][0 ], dp[i - 1 ][1 ] + prices[i]); dp[i][1 ] = Math.max(dp[i - 1 ][1 ], dp[i - 2 ][0 ] - prices[i]); } return dp[prices.length - 1 ][0 ]; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution { public int maxProfit (int [] prices) { int len = prices.length; int dp[][] = new int [2 ][4 ]; dp[0 ][0 ] = -prices[0 ]; for (int i = 1 ; i < len; i++){ dp[i % 2 ][0 ] = Math.max(Math.max(dp[(i - 1 ) % 2 ][0 ], dp[(i - 1 ) % 2 ][1 ] - prices[i]), dp[(i - 1 ) % 2 ][3 ] - prices[i]); dp[i % 2 ][1 ] = Math.max(dp[(i - 1 ) % 2 ][1 ], dp[(i - 1 ) % 2 ][3 ]); dp[i % 2 ][2 ] = dp[(i - 1 ) % 2 ][0 ] + prices[i]; dp[i % 2 ][3 ] = dp[(i - 1 ) % 2 ][2 ]; } return Math.max(Math.max(dp[(len - 1 ) % 2 ][1 ], dp[(len - 1 ) % 2 ][2 ]), dp[(len - 1 ) % 2 ][3 ]); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution { public int maxProfit (int [] prices) { int [] dp=new int [4 ]; dp[0 ] = -prices[0 ]; dp[1 ] = 0 ; for (int i = 1 ; i <= prices.length; i++){ int temp = dp[0 ]; int temp1 = dp[2 ]; dp[0 ] = Math.max(dp[0 ], Math.max(dp[3 ], dp[1 ]) - prices[i-1 ]); dp[1 ] = Math.max(dp[1 ], dp[3 ]); dp[2 ] = temp + prices[i-1 ]; dp[3 ] = temp1; } return Math.max(dp[3 ],Math.max(dp[1 ],dp[2 ])); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution { public int maxProfit (int [] prices) { int [][] dp = new int [prices.length + 1 ][2 ]; dp[1 ][0 ] = -prices[0 ]; for (int i = 2 ; i <= prices.length; i++) { dp[i][0 ] = Math.max(dp[i - 1 ][0 ], dp[i - 2 ][1 ] - prices[i - 1 ]); dp[i][1 ] = Math.max(dp[i - 1 ][1 ], dp[i - 1 ][0 ] + prices[i - 1 ]); } return dp[prices.length][1 ]; } }
714.买卖股票的最佳时机含手续费※ 建议 :
题目链接: https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/ 文章讲解: https://programmercarl.com/0714.%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BA%E5%90%AB%E6%89%8B%E7%BB%AD%E8%B4%B9%EF%BC%88%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%EF%BC%89.html 视频讲解:
题目分析
方案一 在 122.买卖股票的最佳时机II 上增加了一个手续费;对代码的影响就是在计算从持有到不持有的时候需要多减一次手续费。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public int maxProfit (int [] prices, int fee) { int [][] dp = new int [prices.length][2 ]; dp[0 ][0 ] = -prices[0 ]; for (int i = 1 ; i < prices.length; i++) { dp[i][0 ] = Math.max(dp[i - 1 ][0 ], dp[i - 1 ][1 ] - prices[i]); dp[i][1 ] = Math.max(dp[i - 1 ][1 ], dp[i - 1 ][0 ] + prices[i] - fee); } return dp[prices.length - 1 ][1 ]; } }
结果 解答成功: 执行耗时:17 ms,击败了67.69% 的Java用户 内存消耗:54.7 MB,击败了12.91% 的Java用户
分析 时间复杂度: O( n )
空间复杂度: O( nn )
代码随想录 https://programmercarl.com/0714.%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BA%E5%90%AB%E6%89%8B%E7%BB%AD%E8%B4%B9%EF%BC%88%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%EF%BC%89.html
思路 贪心算法 在贪心算法:122.买卖股票的最佳时机II 中使用贪心策略不用关心具体什么时候买卖,只要收集每天的正利润,最后稳稳的就是最大利润了。
而本题有了手续费,就要关系什么时候买卖了,因为计算所获得利润,需要考虑买卖利润可能不足以手续费的情况。
如果使用贪心策略,就是最低值买,最高值(如果算上手续费还盈利)就卖。
此时无非就是要找到两个点,买入日期,和卖出日期。
买入日期:其实很好想,遇到更低点就记录一下。
卖出日期:这个就不好算了,但也没有必要算出准确的卖出日期,只要当前价格大于(最低价格+手续费),就可以收获利润,至于准确的卖出日期,就是连续收获利润区间里的最后一天(并不需要计算是具体哪一天)。
所以我们在做收获利润操作的时候其实有三种情况:
情况一:收获利润的这一天并不是收获利润区间里的最后一天(不是真正的卖出,相当于持有股票),所以后面要继续收获利润。
情况二:前一天是收获利润区间里的最后一天(相当于真正的卖出了),今天要重新记录最小价格了。
情况三:不作操作,保持原有状态(买入,卖出,不买不卖)
贪心算法C++代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution {public : int maxProfit (vector<int >& prices, int fee) { int result = 0 ; int minPrice = prices[0 ]; for (int i = 1 ; i < prices.size (); i++) { if (prices[i] < minPrice) minPrice = prices[i]; if (prices[i] >= minPrice && prices[i] <= minPrice + fee) { continue ; } if (prices[i] > minPrice + fee) { result += prices[i] - minPrice - fee; minPrice = prices[i] - fee; } } return result; } };
从代码中可以看出对情况一的操作,因为如果还在收获利润的区间里,表示并不是真正的卖出,而计算利润每次都要减去手续费,所以要让minPrice = prices[i] - fee;,这样在明天收获利润的时候,才不会多减一次手续费!
大家也可以发现,情况三,那块代码是可以删掉的,我是为了让代码表达清晰,所以没有精简。
本题使用贪心算法并不好理解,也很容易出错,那么我们再来看看是使用动规的方法如何解题。
动态规划 相对于 122.买卖股票的最佳时机II ,本题只需要在计算卖出操作的时候减去手续费就可以了,代码几乎是一样的。
唯一差别在于递推公式部分,所以本篇也就不按照动规五部曲详细讲解了,主要讲解一下递推公式部分。
这里重申一下dp数组的含义:
dp[i][0] 表示第i天持有股票所省最多现金。 dp[i][1] 表示第i天不持有股票所得最多现金
如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来
第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i]
所以:dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
在来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来
第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金,注意这里需要有手续费了 即:dp[i - 1][0] + prices[i] - fee
所以:dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee);
本题和 122.买卖股票的最佳时机II 的区别就是这里需要多一个减去手续费的操作 。
以上分析完毕,C++代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution {public : int maxProfit (vector<int >& prices, int fee) { int n = prices.size (); vector<vector<int >> dp (n, vector <int >(2 , 0 )); dp[0 ][0 ] -= prices[0 ]; for (int i = 1 ; i < n; 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] - fee); } return max (dp[n - 1 ][0 ], dp[n - 1 ][1 ]); } };
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public int maxProfit (int [] prices, int fee) { int len = prices.length; int [][] dp = new int [len][2 ]; dp[0 ][0 ] = -prices[0 ]; for (int i = 1 ; i < len; i++) { dp[i][0 ] = Math.max(dp[i - 1 ][0 ], dp[i - 1 ][1 ] - prices[i]); dp[i][1 ] = Math.max(dp[i - 1 ][0 ] + prices[i] - fee, dp[i - 1 ][1 ]); } return Math.max(dp[len - 1 ][0 ], dp[len - 1 ][1 ]); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public int maxProfit (int [] prices, int fee) { int len = prices.length; int [][] dp = new int [len][2 ]; dp[0 ][0 ] = -prices[0 ] - fee; for (int i = 1 ; i < len; i++) { dp[i][0 ] = Math.max(dp[i - 1 ][0 ], dp[i - 1 ][1 ] - prices[i] - fee); dp[i][1 ] = Math.max(dp[i - 1 ][0 ] + prices[i], dp[i - 1 ][1 ]); } return Math.max(dp[len - 1 ][0 ], dp[len - 1 ][1 ]); }class Solution { public int maxProfit (int [] prices, int fee) { int [] dp = new int [2 ]; dp[0 ] = -prices[0 ]; dp[1 ] = 0 ; for (int i = 1 ; i <= prices.length; i++) { dp[0 ] = Math.max(dp[0 ], dp[1 ] - prices[i - 1 ]); dp[1 ] = Math.max(dp[1 ], dp[0 ] + prices[i - 1 ] - fee); } return dp[1 ]; } }
//使用 2*2 array
class Solution {
public int maxProfit(int[] prices, int fee) {
int dp[][] = new int[2][2];
int len = prices.length;
//[i][0] = holding the stock
//[i][1] = not holding the stock
dp[0][0] = -prices[0];
for(int i = 1; i < len; i++){
dp[i % 2][0] = Math.max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] - prices[i]);
dp[i % 2][1] = Math.max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] + prices[i] - fee);
}
return dp[(len - 1) % 2][1];
}
}
股票问题总结※ https://programmercarl.com/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92-%E8%82%A1%E7%A5%A8%E9%97%AE%E9%A2%98%E6%80%BB%E7%BB%93%E7%AF%87.html