188. 买卖股票的最佳时机 IV(困难)
思路
状态定义
一、首先确定要一天会有几种状态,不难想到有四种:
- a.当天买入了股票;
- b.当天卖出了股票;
- c.当天没有操作,但是之前是买入股票的状态;
- d.当天没有操作,但是之前是卖出股票的状态;
同时操作四种状态有些复杂,通过分析,我们可以优化成两种状态:「当天持有股票」和「当天不持有股票」。这两种状态分别对应为 a/c 和 b/d 。
二、接下来考虑如何表示这两种状态:
- 最常见的方法是使用 dp[0] 和 dp[1] 来表示,但是这种方式不仅不方便识别数组,而且还增加了 dp数组的一个维度,就本题而言,这种定义方式会得到一个三维数组,难以操作。
- 所以建议直接使用能明确表示含义的两个dp数组,比如用 buy 表示持有股票的状态, sell 表示不持有股票的状态。
三、最后确定dp数组的维度:
- 首先必须要有表示天数 i 的维度;
- 还需要有表示买卖次数 k 的维度 j;
四、综上,得到了两个二维的dp数组:
buy[i][j]
表示到第 i 天为止,恰好进行 j 笔交易,并且当前处于持有股票的状态,在这种情况下的最大利润;sell[i][j]
表示到第 i 天为止,恰好进行 j 笔交易,并且当前处于不持有股票的状态,在这种情况下的最大利润;
确定递推公式
在确定递推公式的时候,需要明确一个概念: k什么时候发生变化。这道题默认一买一卖才算是一次完整的交易,所以只有在卖股票的情况下,k才会发生变化。
确定完k的变化后,递推公式就很容易得到,每种状态都由两种情况组合得到:
buy[i][j]
- c.第 i-1 天就持有股票,那么当天收益不变,所得现金就是昨天持有股票的收益:
buy[i-1][j]
; - a.第 i 天买入股票,所得现金就是昨天不持有股票的收益扣去今天的股票价格:
sell[i-1][j] - prices[i]
; - 综合上述两种情况,最大收益就是
max(buy[i-1][j], sell[i-1][j] - prices[i])
。
- c.第 i-1 天就持有股票,那么当天收益不变,所得现金就是昨天持有股票的收益:
sell[i][j]
- d. 第 i-1 天就不持有股票,那么当天收益不变,所得现金就是昨天不持有股票的收益:
sell[i-1][j]
; - b. 第 i 天售出股票,那么所得现金就是昨天持有股票的收益加上今天的股票价格:
buy[i-1][j-1] + prices[i]
; - 综合上述两种情况,最大收益就是
max(sell[i-1][j], buy[i-1][j-1] + prices[i])
。
- d. 第 i-1 天就不持有股票,那么当天收益不变,所得现金就是昨天不持有股票的收益:
根据我们对 k 的变化的定义,体现在递推公式中就是在推导 sell[i][j]
的第二种情况时用到的 buy[i-1][j-1]
,因为在卖出股票的时候,就完成了一次交易,因此k的次数加一,所以上一个持有股票的状态就是 j-1 。
dp数组的初始化
-
将所有的
buy[0][0...k]
和sell[0][0...k]
都设置为边界; -
对于
buy[0][0...k]
,由于对应 i = 0 ,因此只有 prices[0] 唯一的股价,所以如果要持有股票,只能选择买入,因此对于 k = 0,buy[0][0] = - prices[0]
。由于只有一天,不可能完成一次完整的交易(即不可能售出),所以对于任意的 k >= 1 ,将所有的buy[0][1...k]
设置一个非常小的值(这里使用 INT_MIN / 2) ,表示不合法的状态。 -
同理,对于
sell[0][0...k]
,对应于 i=0 ,只有 prices[0] ,所以如果要不持有股票,只能不进行任何操作,所以对于 k=0,sell[0][0] = 0
,所以对于任意的 k >= 1 ,同样是不合法状态,将所有的buy[0][1...k]
设置一个非常小的值(这里使用 INT_MIN / 2) 。
最终的返回结果
在遍历完所有的 prices 后,手上不持有股票对应的最大力利润一定严格大于手上持有股票对应的最大利润,然而完成的交易数不是越多越好(比如数组 prices 单调递减,此时不进行任何交易才是最优解),因此最终答案是 sell[n-1][0...k]
的最大值,即 return *max_element(sell[n-1].begin(), sell[n-1].end());
。
易错点
一、k 的范围:
由于一次完整的交易需要两天,如果交易次数 k 大于 n/2 ,则必然有一天交易了两次,这是没有意义的,因此我们可以减小 k 的值,再进行动态规划,即 k = min(k, n/2);
。
二、buy 数组的维数:
第二个维度是 k+1 维,表示 k 的范围从 0~k 。如果数组单调递减,此时不进行任何交易是最优解, k=0;
三、j 的循环:
由于 sell[i][j]
的状态转移方程中包含 buy[i-1][j-1]
,如果 j=0 时,这是一个不合法状态,所以无需对 sell[i][j] 进行状态转移,让它保持值为 0 即可,所以在 j=0 时,只需要对 buy[i][0] 单独处理。
代码
class Solution {
public:int maxProfit(int k, vector<int>& prices) {int n = prices.size();if(n == 0) return 0;vector<vector<int>> buy(n, vector<int>(k+1));vector<vector<int>> sell(n, vector<int>(k+1));// k值的预处理k = min(k, n/2);// 预处理buy[0][0] = -prices[0];sell[0][0] = 0;for(int i=1; i<=k; ++i){// 不能设置为 INT_MINbuy[0][i] = sell[0][i] = INT_MIN / 2;}for(int i=1; i<n; ++i){buy[i][0] = max(buy[i-1][0], sell[i-1][0] - prices[i]);for(int j=1; j<=k; ++j){buy[i][j] = max(buy[i-1][j], sell[i-1][j] - prices[i]);sell[i][j] = max(sell[i-1][j], buy[i-1][j-1] + prices[i]);}}return *max_element(sell[n-1].begin(), sell[n-1].end());}
};
空间压缩
从上述代码不难发现,当前持有股票和当前不持有股票的最大收益都只和前一个状态有关,buy[i][j] = max(buy[i-1][j], sell[i-1][j] - prices[i]); sell[i][j] = max(sell[i-1][j], buy[i-1][j-1] + prices[i]);
,因此可以对两个数组进行空间压缩,只留下第二个维度。
代码如下:
class Solution {
public:int maxProfit(int k, vector<int>& prices) {int n = prices.size();if(n == 0) return 0;vector<int> buy(k+1);vector<int> sell(k+1);// k值的预处理k = min(k, n/2);// 预处理buy[0] = -prices[0];sell[0] = 0;for(int i=1; i<=k; ++i){// 不能设置为 INT_MINbuy[i] = sell[i] = INT_MIN / 2;}for(int i=1; i<n; ++i){buy[0] = max(buy[0], sell[0] - prices[i]);for(int j=1; j<=k; ++j){buy[j] = max(buy[j], sell[j] - prices[i]);sell[j] = max(sell[j], buy[j-1] + prices[i]);}}return *max_element(sell.begin(), sell.end());}
};
参考资料:188. 买卖股票的最佳时机 IV