[LeetCode] 188. Best Time to Buy and Sell Stock IV

You are given an integer array prices where prices[i] is the price of a given stock on the ith day, and an integer k.

Find the maximum profit you can achieve. You may complete at most k transactions.

Note: You may not engage in multiple transactions simultaneously (i.e., you must sell the stock before you buy again).

Example 1:

Input: k = 2, prices = [2,4,1]
Output: 2
Explanation: Buy on day 1 (price = 2) and sell on day 2 (price = 4), profit = 4-2 = 2.

Example 2:

Input: k = 2, prices = [3,2,6,5,0,3]
Output: 7
Explanation: Buy on day 2 (price = 2) and sell on day 3 (price = 6), profit = 6-2 = 4. Then buy on day 5 (price = 0) and sell on day 6 (price = 3), profit = 3-0 = 3.

Constraints:

  • 0 <= k <= 100
  • 0 <= prices.length <= 1000
  • 0 <= prices[i] <= 1000

买卖股票的最佳时机IV。

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

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

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

这是股票系列的第四题了,题目基本设定还是跟前三个版本一样,但是这道题问的是如果你最多可以交易K次,请问你的最大收益是多少。

经过前面的题,这道题依然需要用动态规划做。我参考了一个讲的很好的题解。这里我们第一次接触到三维的 DP。这里 dp[i][j][K] 的定义是:到下标为 i 的天数为止(从 0 开始),到下标为 j 的交易次数(从 0 开始),以状态为K的情况下,最大收益是多少。这里的状态K用 0 或 1 分别表示不持有和持有股票,也只有这两种状态。

这个题有几个边界条件需要排除。当 K == 0 的时候,也就是没有交易次数的时候,最大收益就是 0,因为没有买也没有卖。当 K >= 数组长度的一半的时候,这道题就转化为了版本二的贪心算法。因为K的次数包含了买和卖,换言之一次买卖就得占用两次交易次数,所以当 K 大于数组长度的一半的时候,用贪心算法算出最大收益即可,多余的 K 是用不上的。

接下来是处理几种一般情况,

当 i == 0 的时候,也就是第 0 天的时候,如果当前是持有状态,那么初始值就是 -prices[0];如果当前是不持有状态,初始值是 0

当 i 不为 0 但是持有股票[i][j][1]的话,分两种情况

如果交易次数 j == 0,也就是不允许交易的话,当前位置持股的 DP 值 dp[i][j][1] 是前一天持股的 DP 值 dp[i - 1][j][1] 和今天买了股票 -prices[i] 之间的较大值

如果交易次数 j 不为 0,那么当前位置持股的 DP 值 dp[i][j][1] 是前一天持股的 DP 值 dp[i - 1][j][1] 和前一天不持股但是买了股票 dp[i - 1][j - 1][0] - prices[i] 之间的较大值

对于其他情形,也就是在某一天不持有股票的话 dp[i][j][0],DP 值就是 前一天不持有股票 dp[i - 1][j][0] 和 前一天持有股票但是今天卖掉了 dp[i][j - 1][1] + prices[i] 之间的较大值

因为最后返回的时候,利益更大的一定是在不持有的这个状态上,所以返回的是 dp[len - 1][k - 1][0]

时间O(n^2)

空间O(n^3) - 三维矩阵,很可能会超出空间的限制

Java实现

 1 class Solution {
 2     public int maxProfit(int k, int[] prices) {
 3         int len = prices.length;
 4         // 特判
 5         if (k == 0 || len < 2) {
 6             return 0;
 7         }
 8         if (k >= len / 2) {
 9             return greedy(prices, len);
10         }
11 
12         // dp[i][j][K]:到下标为 i 的天数为止(从 0 开始),到下标为 j 的交易次数(从 0 开始)
13         // 状态为 K 的最大利润,K = 0 表示不持股,K = 1 表示持股
14         int[][][] dp = new int[len][k][2];
15         // 初始化:把持股的部分都设置为一个较大的负值
16         for (int i = 0; i < len; i++) {
17             for (int j = 0; j < k; j++) {
18                 dp[i][j][1] = -9999;
19             }
20         }
21 
22         // 编写正确代码的方法:对两个"基本状态转移方程"当 i - 1 和 j - 1 分别越界的时候,做特殊判断,赋值为 0 即可
23         for (int i = 0; i < len; i++) {
24             for (int j = 0; j < k; j++) {
25                 if (i == 0) {
26                     dp[i][j][1] = -prices[0];
27                     dp[i][j][0] = 0;
28                 } else {
29                     if (j == 0) {
30                         dp[i][j][1] = Math.max(dp[i - 1][j][1], -prices[i]);
31                     } else {
32                         // 基本状态转移方程 1
33                         dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
34                     }
35                     // 基本状态转移方程 2
36                     dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
37                 }
38             }
39         }
40         // 说明:i、j 状态都是前缀性质的,只需返回最后一个状态
41         return dp[len - 1][k - 1][0];
42     }
43 
44     private int greedy(int[] prices, int len) {
45         // 转换为股票系列的第 2 题,使用贪心算法完成,思路是只要有利润,就交易
46         int res = 0;
47         for (int i = 1; i < len; i++) {
48             if (prices[i - 1] < prices[i]) {
49                 res += prices[i] - prices[i - 1];
50             }
51         }
52         return res;
53     }
54 }

 

看到的一个比较好理解的三维 DP 的做法。关于 K 超过了数组长度的一半的 corner case 的判定,参见第一种做法。我介绍一下三维 DP 的思路。dp[n][2][k + 1] 的定义是第 i 天交易了 k 次之后,手上持有 1 /不持有 0 股票的最大利润。

这个做法中,我们一开始把 k 初始化成 Math.min(k, n / 2) ,让 k 只在买入的时候 + 1,这里的思路是只有卖出了上一次持有的股票,你才能再次购买。

这里我们需要一个两层 for 循环,第一层遍历所有的 price,第二层遍历买卖的次数 k。其余解释参见代码注释。

时间O(n^2)

空间O(n^3)

Java实现

 1 class Solution {
 2     public int maxProfit(int k, int[] prices) {
 3         int n = prices.length;
 4         // corner case
 5         if (n <= 1) {
 6             return 0;
 7         }
 8         
 9         // 因为一次交易至少涉及两天,所以如果k大于总天数的一半,就直接取天数一半即可,多余的交易次数是无意义的
10         k = Math.min(k, n / 2);
11         
12         // dp定义:dp[i][j][k]代表 第i天交易了k次时的最大利润,其中j代表当天是否持有股票,0不持有,1持有
13         // 只有买的时候才计算交易次数
14         int[][][] dp = new int[n][2][k + 1];
15         for (int i = 0; i <= k; i++) {
16             dp[0][0][i] = 0;
17             dp[0][1][i] = -prices[0];
18         }
19         
20         for (int i = 1; i < n; i++) {
21             for (int j = 1; j <= k; j++) {
22                 // 今天不持有的收益 = 前一天不持有的收益 OR 前一天卖出股票的收益
23                 dp[i][0][j] = Math.max(dp[i - 1][0][j], dp[i - 1][1][j] + prices[i]);
24                 // 今天持有的收益 = 前一天持有的收益 OR 前一天买入的收益
25                 dp[i][1][j] = Math.max(dp[i - 1][1][j], dp[i - 1][0][j - 1] - prices[i]);
26             }
27         }
28         return dp[n - 1][0][k];
29     }
30 }
31 
32 // https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/solution/javayi-ge-si-lu-da-bao-suo-you-gu-piao-t-pd1p/

 

这里我再提供一个二维DP的做法,思路类似。这里我们还是设一个DP数组,定义是 i 次交易以后手上持有/不持有股票的收益。首先还是处理 K >= len / 2 的 corner case。对于一般的 case,对于当前的某个价钱 price 而言,如果恰好他是第 j 次买入(我们把买一次 + 卖一次定义为一次交易),最大收益应该是 dp[j][1] = Math.max(dp[j][1], dp[j - 1][0] - price),意思是第 j 次的买入的最大值有可能是不操作上一次买入之间较大的那一个。

如果恰好他是第 j 次卖出,最大收益应该是 dp[j][0] = Math.max(dp[j][0], dp[j][1] + price),意思是第 j 次的卖出的最大值有可能是不操作或者此时卖出之间较大的那一个。

时间O(n^2)

空间O(n^2)

Java实现

 1 class Solution {
 2     public int maxProfit(int k, int[] prices) {
 3         int len = prices.length;
 4         // corner case
 5         if (k == 0 || len < 2) {
 6             return 0;
 7         }
 8         if (k >= len / 2) {
 9             return greedy(prices);
10         }
11 
12         // normal case
13         // i次交易以后手上持有/不持有股票的收益
14         int[][] dp = new int[k + 1][2];
15         for (int i = 0; i <= k; i++) {
16             dp[i][1] = Integer.MIN_VALUE;
17         }
18         for (int price : prices) {
19             for (int j = 1; j <= k; j++) {
20                 dp[j][1] = Math.max(dp[j][1], dp[j - 1][0] - price);
21                 dp[j][0] = Math.max(dp[j][0], dp[j][1] + price);
22             }
23         }
24         return dp[k][0];
25     }
26 
27     private int greedy(int[] prices) {
28         int res = 0;
29         for (int i = 1; i < prices.length; i++) {
30             if (prices[i] > prices[i - 1]) {
31                 res += prices[i] - prices[i - 1];
32             }
33         }
34         return res;
35     }
36 }

 

LeetCode 题目总结

posted @ 2020-09-20 06:41  CNoodle  阅读(365)  评论(0)    收藏  举报