力扣 188 买卖股票的最佳时机 IV 题解

91 阅读5分钟

188. 买卖股票的最佳时机 IV - 力扣(LeetCode)

给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

思路

第一阶段

我们将这个题目拆解来看。首先,有一个很明显的点,那就是,只有上升区间值得关注,这个区间的特点是,不管是买入时间提前、还是卖出时间点延后都会导致利润的减少。他们看起来就像:

image.png 我们想想就知道,我们买入股票的时间点只会是上升区间的最低点,卖出股票的时间点只会是上升区间的最高点,在其他节点的买入卖出操作都是没有意义的。

可以想见,加入上升区间数量为 m,那么只要交易次数 k 大于等于 m,那么我们能够获得的最大利润就是所有上升区间的利润之和,且不会随着 k 的增大而继续提升。因为没有利润空间了。

而一旦 k 小于 m,那么我们就需要考虑如何合理的利用交易次数,使得利润最大化。所谓 “合理化”,就是放弃的艺术,我们有两种选择来减少交易次数:

  1. 直接不使用某个上升区间。
  2. 将多个上升区间合并。

合并的意思是,将多个上升区间合并成一个大的上升区间,这样就能够节省交易次数。

image.png

现在问题就转化成了:怎样合并上升区间能够使得利润最大化?

第二阶段

好的,现在假如我们已经获取到了包含有所有上升区间的数组,我们先约定一些写法:

ascending:包含所有上升区间的数组ascending_i:i个上升区间ascending_i_m:第i个上升区间的最高点ascending_i_n:第i个上升区间的最低点ascending:包含所有上升区间的数组 \\ ascending\_i: 第 i 个上升区间 \\ ascending\_i\_m:第 i 个上升区间的最高点 \\ ascending\_i\_n:第 i 个上升区间的最低点 \\

然后我们需要知道,怎样买入股票才能够获取最大的利润。现在我们做出新的约定:

dp(i,j)1i区间内,进行j次交易的最大利润dp(i, j):1 到i 区间内,进行 j 次交易的最大利润 \\

动态规划,本质就是找到状态转移方程,就是找到如何通过前几个状态推导出当前状态。

对于第 i 个区间,我们有几种处理方法:

  1. 抛弃这个区间,那么 dp(i, j) = dp(i-1, j)
  2. 选择这个区间,但这个区间可以单独取,也可以和前面的区间合并。

分情况讨论

情况一很好理解,抛弃了 i 区间,那么此时 1i 区间的最大利润,自然和 1i-1 区间一样。我们单独说说情况二。

在情况二中,选择了区间 i,此时利润的计算方式和向前合并多个区间有关:

(1)不向前合并,也就是这个区间单独取,那么此时,利润就是

dp(i,j)=ascending_i_mascending_i_n+dp(i1,j1)dp(i, j) = ascending\_i\_m - ascending\_i\_n + dp(i-1, j-1)

简单解释,就是区间 i 能够获取的利润加上 1i-1 区间的最大利润,但由于 i 区间是单独取的,所以 1i-1 区间的可用交易次数减少了 1。

(2)向前合并一个区间,那么此时,利润就是

dp(i,j)=ascending_i_mascending_i1_n+dp(i2,j1))dp(i,j) = ascending\_i\_m - ascending\_i-1\_n + dp(i-2, j-1))

由于 i-1 区间和 i 区间合并,那么此时这个合并区间的利润自然就是 i 区间的最高点减去 i-1 区间的最低点,再加上 1i-2 区间的最大利润,但由于 i-1 区间和 i 区间合并,所以 1i-2 区间的可用交易次数减少了 1。

(3)向前合并两个区间,那么此时,利润就是

dp((i,j)=ascending_i_mascending_i2_n+dp(i3,j1)))dp((i,j) = ascending\_i\_m - ascending\_i-2\_n + dp(i-3, j-1)))

(n)依次类推,直到合并了 t 个区间,那么此时,利润就是

dp(i,j)=ascending_i_mascending_it_n+dp(it1,t1))dp(i,j) = ascending\_i\_m - ascending\_i-t\_n + dp(i-t-1, t-1))

能够类推到没有可以向前合并的区间。

不必看到公式就慌张,牢记我们在干什么。我们需要找到怎么合并才能够使在取到区间 i 的前提下,利润 dp(i, j) 最大。当然我们可以直接遍历找最大值,但是这样会提高时间复杂度,我们看看这个公式,我们发现:

image.png

所以,要使 dp(i, j) 最大,就是要找到,到 i 为止,dp(i-t-1, t-1)) - ascending_i-t_n 的最大值。

我们本来就在顺着 i 依次进行动态规划,拿一个变量记录一下这个最大值自然是顺手的事儿。

代码

 /**
      * 188 买卖股票的最佳时机IV
      *
      * @param k
      * @param prices
      * @return
      */
 public int maxProfit(int k, int[] prices) {
     if (prices == null || prices.length <= 1 || k <= 0) {
         return 0;
     }
 ​
     // 第一阶段:获取所有上升区间
     List<int[]> ascending = new ArrayList<>();
     int slow = 0;
     int fast = 0;
     while (fast < prices.length) {
         while (fast < prices.length - 1 && prices[fast + 1] > prices[fast]) {
             fast++;
         }
         if (prices[fast] > prices[slow]) {
             ascending.add(new int[]{prices[slow], prices[fast]});
         }
         fast++;
         slow = fast;
     }
     // 处理没有上升区间的情况
     if (ascending.isEmpty()) {
         return 0;
     }
 ​
     int n = ascending.size();
     int m = Math.min(n, k);             // 剪枝。之前说了 k 如果大于 n,超出部分没有意义不会使利润增加
     int[][] dp = new int[n][m + 1];     // 记录 1~i 区间,交易 j 次最大利润
 ​
     for (int j = 1; j <= m; j++) {
         // 用这个变量记录情况二提到的那个最大值
         int maxDiff = -ascending.get(0)[0];
         dp[0][j] = ascending.get(0)[1] - ascending.get(0)[0];
 ​
         for (int i = 1; i < n; i++) {
             // 更新maxDiff以避免内层循环
             maxDiff = Math.max(maxDiff, dp[i-1][j-1] - ascending.get(i)[0]);
 ​
             dp[i][j] = Math.max(
                 dp[i-1][j],     // 情况一
                 ascending.get(i)[1] + maxDiff      // 情况二
             );
         }
     }
 ​
     return dp[n-1][m];
 }