股票的最佳买卖时间III

原文链接

题目描述:

给定一个数组, 它的第i个元素是一支给定的股票在第i天的价格。设计一个算法来计算你所能获取的最大利润,最多可完成两笔交易,且不能同时参与多笔交易,即再次购买之前必须出售掉之前的股票。

问题分析:

得到该问题的动态转移方程并不难:
dp[k, i]=max⁡(dp[k, i −1], prices[i]−prices[j]+dp[k −1, j −1]), j=[0..i −1]

对于k笔交易,在第i天:
• 如果我们选择不交易,那么收益跟前一天保持一致为dp[k, i-1]
• 如果我们在j天买了股票(j=[0..i-1]),然后在第i天卖出了,那么我们的收益为 prices[i]−prices[j]+dp[k−1, j −1].

实际上j可以等于i,当j=i时,prices[i]−prices[j]+dp[k−1, j]= dp[k −1, i],看上去像是减少了一次交易机会。
我看到有些人在使用公式dp[k, i]=ma x⁡(dp[k, i −1], prices[i]−prices[j]+dp[k−i, j]),而不是dp[k −i, j −1].实际上这没有多大意义,如果股票在第j天被买入了,那么上一次交易的总收益应该与第j-1天的收益一样。当然,基于这种考虑的动态转移方程也是正确的,因为如果股票在第j天被出售了然后在当天又买回来了,这种情况与第j天没有进行交易是一样的。

所以,最直观的实现方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int maxProfitDp(int[] prices) {
if(prices.length == 0) return 0;
int[][] dp = new int[3][prices.length];
for(int k = 1; k <= 2; k++) {
for(int i = 1; i < prices.length; i++) {
int min = prices[0];
for(int j = 1; j <= i; j++) {
min = Math.min(min, prices[j] - dp[k - 1][j - 1]);
}
dp[k][i] = Math.max(dp[k][i - 1], prices[i] - min);
}
}
return dp[2][prices.length - 1];
}

时间复杂度为O(kn^2),空间复杂度为O(kn).

在上述代码中,min被重复计算了,我们可以进行优化:

1
2
3
4
5
6
7
8
9
10
11
12
public int maxProfitDp(int[] prices) {
if(prices.length == 0) return 0;
int[][] dp = new int[3][prices.length];
for(int k = 1; k <= 2; k++) {
int min = prices[0];
for(int i = 1; i < prices.length; i++) {
min = Math.min(min, prices[i] - dp[k - 1][i - 1]);
dp[k][i] = Math.max(dp[k][i - 1], prices[i] - min);
}
}
return dp[2][prices.length - 1];
}

时间复杂度为O(kn),空间复杂度为O(kn).

如果我们将两个for循环进行交换:

1
2
3
4
5
6
7
8
9
10
11
12
13
public int maxProfitDp(int[] prices) {
if(prices.length == 0) return 0;
int[][] dp = new int[3][prices.length];
int[] min = new int[3];
Arrays.fill(min, prices[0]);
for(int i = 1; i < prices.length; i++) {
for(int k = 1; k <= 2; k++) {
min[k] = Math.min(min[k], prices[i] - dp[k - 1][i - 1]);
dp[k][i] = Math.max(dp[k, i - 1], prices[i] - min[k]);
}
}
return dp[2][prices.length - 1];
}

我们需要在每次交易时都保存最小值min,所以会保存k个min。
我们发现二维数组的第二维i的值仅取决于前一个i-1的值,所以我们可以进行纬度压缩。(我们也可以压缩第一纬度k,因为第k个位置也是仅取决于第k-1个位置。但是两个维度不能同时压缩)

1
2
3
4
5
6
7
8
9
10
11
12
13
public int maxProfitDp(int[] prices) {
if(prices.length == 0) return 0;
int[] dp = new int[3];
int[] min = new int[3];
Arrays.fill(min, prices[0]);
for(int i = 1; i < prices.length; i++) {
for(int k = 1; k <= 2; k++) {
min[k] = Math.min(min[k], prices[i] - dp[k - 1]);
dp[k] = Math.max(dp[k], prices[i] - min[k]);
}
}
return dp[2];
}

此时时间复杂度为O(kn),空间复杂度为O(k).

在这道题中,k=2.我们可以将数组直接用变量代替

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int maxProfitDp(int[] prices) {
int buy1 = Integer.MAX_VALUE;
int buy2 = Integer.MAX_VALUE;
int sell1 = 0;
int sell2 = 0;
for(int i = 0; i < prices.length; i++) {
buy1 = Math.min(buy1, prices[i]);
sell1 = Math.max(sell1, prices[i] - buy1);
buy2 = Math.min(buy2, prices[i] - sell1);
sell2 = Math.max(sell2, prices[i] - buy2);
}
return sell2;
}

我们也可以用其他的方式来解释上面的代码。每一天我们都尽可能地买入最低价股票,并且尽可能地用最高价卖出。对于第二次交易,我们把第一次交易的盈利放入第二次交易的成本中(做差,第二次的成本减第一次的盈利prices[i]−sell1),那么我们第二次交易的的盈利就是两次交易的盈利。