[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 }