188. 买卖股票的最佳时机 IV - 力扣(LeetCode)
给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
思路
第一阶段
我们将这个题目拆解来看。首先,有一个很明显的点,那就是,只有上升区间值得关注,这个区间的特点是,不管是买入时间提前、还是卖出时间点延后都会导致利润的减少。他们看起来就像:
我们想想就知道,我们买入股票的时间点只会是上升区间的最低点,卖出股票的时间点只会是上升区间的最高点,在其他节点的买入卖出操作都是没有意义的。
可以想见,加入上升区间数量为 m,那么只要交易次数 k 大于等于 m,那么我们能够获得的最大利润就是所有上升区间的利润之和,且不会随着 k 的增大而继续提升。因为没有利润空间了。
而一旦 k 小于 m,那么我们就需要考虑如何合理的利用交易次数,使得利润最大化。所谓 “合理化”,就是放弃的艺术,我们有两种选择来减少交易次数:
- 直接不使用某个上升区间。
- 将多个上升区间合并。
合并的意思是,将多个上升区间合并成一个大的上升区间,这样就能够节省交易次数。
现在问题就转化成了:怎样合并上升区间能够使得利润最大化?
第二阶段
好的,现在假如我们已经获取到了包含有所有上升区间的数组,我们先约定一些写法:
然后我们需要知道,怎样买入股票才能够获取最大的利润。现在我们做出新的约定:
动态规划,本质就是找到状态转移方程,就是找到如何通过前几个状态推导出当前状态。
对于第 i 个区间,我们有几种处理方法:
- 抛弃这个区间,那么 dp(i, j) = dp(i-1, j)
- 选择这个区间,但这个区间可以单独取,也可以和前面的区间合并。
分情况讨论
情况一很好理解,抛弃了 i 区间,那么此时 1i 区间的最大利润,自然和 1i-1 区间一样。我们单独说说情况二。
在情况二中,选择了区间 i,此时利润的计算方式和向前合并多个区间有关:
(1)不向前合并,也就是这个区间单独取,那么此时,利润就是
简单解释,就是区间 i 能够获取的利润加上 1i-1 区间的最大利润,但由于 i 区间是单独取的,所以 1i-1 区间的可用交易次数减少了 1。
(2)向前合并一个区间,那么此时,利润就是
由于 i-1 区间和 i 区间合并,那么此时这个合并区间的利润自然就是 i 区间的最高点减去 i-1 区间的最低点,再加上 1i-2 区间的最大利润,但由于 i-1 区间和 i 区间合并,所以 1i-2 区间的可用交易次数减少了 1。
(3)向前合并两个区间,那么此时,利润就是
(n)依次类推,直到合并了 t 个区间,那么此时,利润就是
能够类推到没有可以向前合并的区间。
不必看到公式就慌张,牢记我们在干什么。我们需要找到怎么合并才能够使在取到区间 i 的前提下,利润 dp(i, j) 最大。当然我们可以直接遍历找最大值,但是这样会提高时间复杂度,我们看看这个公式,我们发现:
所以,要使 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];
}