算法: 买卖股票III 123. Best Time to Buy and Sell Stock III
Posted AI架构师易筋
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法: 买卖股票III 123. Best Time to Buy and Sell Stock III相关的知识,希望对你有一定的参考价值。
123. Best Time to Buy and Sell Stock III
You are given an array prices where prices[i] is the price of a given stock on the ith day.
Find the maximum profit you can achieve. You may complete at most two transactions.
Note: You may not engage in multiple transactions simultaneously (i.e., you must sell the stock before you buy again).
Example 1:
Input: prices = [3,3,5,0,0,3,1,4]
Output: 6
Explanation: Buy on day 4 (price = 0) and sell on day 6 (price = 3), profit = 3-0 = 3.
Then buy on day 7 (price = 1) and sell on day 8 (price = 4), profit = 4-1 = 3.
Example 2:
Input: prices = [1,2,3,4,5]
Output: 4
Explanation: Buy on day 1 (price = 1) and sell on day 5 (price = 5), profit = 5-1 = 4.
Note that you cannot buy on day 1, buy on day 2 and sell them later, as you are engaging multiple transactions at the same time. You must sell before buying again.
Example 3:
Input: prices = [7,6,4,3,1]
Output: 0
Explanation: In this case, no transaction is done, i.e. max profit = 0.
Example 4:
Input: prices = [1]
Output: 0
Constraints:
- 1 <= prices.length <= 105
- 0 <= prices[i] <= 105
1. 低买高卖逻辑分析
一开始我们计算 minimumBuyPrice2 的部分让我很困惑。然后我尝试自己研究数学,看看它是怎么回事。希望下面的解释对和我有同样困惑的人有所帮助。
所以,在下面的代码中,lowestBuyPrice1 和 maxProfit1 很容易理解。唯一可能需要时间来理解的是最低购买价格 2 的计算。这里的 minimumBuyPrice2 实际上不是我们在第二笔交易中购买股票的确切价格。实际上,如果我们可以打开它,它包含两部分
"lowestBuyPrice2" = buyPrice2 - maxProfit1
= buyPrice2 - (highestSellPrice1 - lowestBuyPrice1).
所以你会看到,“lowestBuyPrice2”包含第二笔交易的买入价以及我们第一笔交易获得的利润。当我们计算
"maxProfit2" = sellPrice2 - lowestBuyPrice2
= sellPrice2 - buyPrice2 + maxProfit1
= profitOf2ndTrans + maxProfit1.
因此,在计算结束时,“maxProfit2”将包含两个交易的利润。
为避免用于这些变量的名称出现歧义,我使用斜体来分隔变量(“lowestBuyPrice2”、“maxProfit2”),这些变量的含义可能会令人困惑。
最终解法如下:
public class Solution
public int maxProfit(int[] prices)
int maxProfit1 = 0;
int maxProfit2 = 0;
int lowestBuyPrice1 = Integer.MAX_VALUE;
int lowestBuyPrice2 = Integer.MAX_VALUE;
for(int p:prices)
maxProfit2 = Math.max(maxProfit2, p-lowestBuyPrice2);
lowestBuyPrice2 = Math.min(lowestBuyPrice2, p-maxProfit1);
maxProfit1 = Math.max(maxProfit1, p-lowestBuyPrice1);
lowestBuyPrice1 = Math.min(lowestBuyPrice1, p);
return maxProfit2;
2. 扩展为K笔交易
上面2笔交易是个特例,可以扩展为如下通用方法。
class Solution
public int maxProfit(int[] prices)
return maxProfit(2, prices);
public int maxProfit(int k, int[] prices)
int[] s = new int[k+1];
int[] b = new int[k+1];
for(int i=0; i<b.length; i++)
b[i] = Integer.MIN_VALUE;
for(int p : prices)
for(int i=k; i>=1; i--)
s[i] = Math.max(s[i], b[i]+p);
b[i] = Math.max(b[i], s[i-1]-p);
return s[k];
3. 状态机的简单 DP 解决方案,O(n) 时间复杂度,O(1) 空间复杂度
这种方法可以用于所有基于股票价格的问题。
这个想法是设计一个正确描述问题陈述的状态机。
状态图背后的直觉:
我们从 state 开始0,在那里我们可以休息(即什么都不做)或以给定的价格购买股票。
- 如果我们选择休息,我们保持状态 0
- 如果我们买,我们花一些钱(当天的股票价格)然后去状态 1
从状态1,我们可以再次选择什么都不做,或者我们可以出售我们的股票。
- 如果我们选择休息,我们保持状态 1
- 如果我们卖了,我们赚了一些钱(当天的股票价格)然后去状态 2
这为我们完成了一笔交易。请记住,我们唯一能做的atmost 2交易。
从状态上2,我们可以选择什么都不做或购买更多的股票。
- 如果我们选择休息,我们保持状态 2
- 如果我们买,我们去状态 3
从状态3,我们可以再次选择什么都不做,或者我们可以最后一次出售我们的股票。
- 如果我们选择休息,我们保持状态 3
- 如果我们出售,我们已经利用了我们允许的交易并达到了最终状态 4
从状态图到代码
// Assume we are in state S
// If we buy, we are spending money but we can also choose to do nothing
// Doing nothing means going from S->S
// Buying means going from some state X->S, losing some money in the process
S = max(S, X-prices[i])
// Similarly, for selling a stock
S = max(S, X+prices[i])
代码:
int maxProfit(vector<int>& prices)
if(prices.empty()) return 0;
int s1=-prices[0],s2=INT_MIN,s3=INT_MIN,s4=INT_MIN;
for(int i=1;i<prices.size();++i)
s1 = max(s1, -prices[i]);
s2 = max(s2, s1+prices[i]);
s3 = max(s3, s2-prices[i]);
s4 = max(s4, s3+prices[i]);
return max(0,s4);
我们可以创建 4 个变量,每个状态一个,不包括初始状态,因为它总是 0,初始化s1
为-prices[0]
,其余为,INT_MIN
因为它们稍后会被覆盖。
为了达到s1
,我们要么留在原地,要么s1
第一次购买股票。
为了达到s2
,我们要么呆在里面,s2
要么我们卖掉,s1
然后到s2
类似地为s3和s4。
最后,我们返回s4
或更准确地说,max(0,s4)
因为我们初始化s4
为INT_MIN
。
这个想法适用于股票的所有问题,只要我们的状态图是正确的,我们就可以这样编码。
旁注:从技术上讲,这是一种动态编程方法,我们实际上应该这样做,s2[i] = max(s2[i-1], s1[i-1]+prices[i])
但我们可以放心, 的覆盖值s1将始终比前一个好,因此我们不需要临时变量。
4. 动态规划 – 从最多1次交易到3次交易的推演
https://www.youtube.com/watch?v=oDhu5uGq_ic&ab_channel=CodingTech
不难得到DP递归公式:
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] = max(dp[k, i-1],prices[i] -prices[j] + dp[k-1, j])
,最后一个是dp[k-1, j]
而不是 dp[k-1, j-1]
。这不是直接意义上的,如果股票是在第 j 天买的,那么之前交易的总利润应该在第 (j-1) 天完成。然而,基于该公式的结果也是正确的,因为如果股票在第 j 天卖出然后再次买入,那么如果我们当天不交易,结果也是一样的。
所以直接的实现是:
4.1 时间复杂度为 O(kn^2),空间复杂度为 O(kn)。
class Solution
public int maxProfit(int[] prices)
int n=prices.length;
if(n==0) return 0;
int[][] dp=new int[3][n];
for (int k=1;k<=2;k++)
for (int i=1;i<n;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][n-1];
4.2 在上面的代码中,min是重复计算的。它可以很容易地改进为:
时间复杂度为 O(kn),空间复杂度为 O(kn)。
class Solution
public int maxProfit(int[] prices)
int n=prices.length;
if(n==0) return 0;
int[][] dp=new int[3][n];
for (int k=1;k<=2;k++)
int min=prices[0];
for (int i=1;i<n;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][n-1];
4.3 我们需要为每笔交易节省 min,因此有 k 个“min”。
我们可以发现第二个维度(变量 i)只依赖于前一个维度(i-1),所以我们可以压缩这个维度。(我们也可以选择第一个维度(变量 k),因为它也只依赖于前一个维度 k-1,但不能同时压缩这两个维度。)
所以时间复杂度是O(kn),空间复杂度变成O(k)。
class Solution
public int maxProfit(int[] prices)
int n=prices.length;
if(n==0) return 0;
int[][] dp=new int[3][n];
int[] min=new int[3];
Arrays.fill(min,prices[0]);
for (int i=1;i<n;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][n-1];
4.4 在这种情况下,K 为 2。我们可以将数组扩展为所有命名变量:
时间复杂度是O(kn),空间复杂度变成O(k)。
我们也可以换一种说法来解释上面的代码。每天,我们都以尽可能低的价格买入股票,并以尽可能高的价格卖出股票。对于第二笔交易,我们将第一笔交易的利润整合到第二笔买入的成本中,那么第二笔卖出的利润就是两次交易的总利润。
class Solution
public int maxProfit(int[] prices)
int n=prices.length;
if(n==0) return 0;
int[] dp=new int[3];
int[] min=new int[3];
Arrays.fill(min,prices[0]);
for (int i=1;i<n;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];
//Version 5
class Solution
public int maxProfit(int[] prices)
int buy1 = Integer.MAX_VALUE, buy2 = Integer.MAX_VALUE;
int sell1 = 0, 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;
参考
https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/discuss/39611/Is-it-Best-Solution-with-O(n)-O(1).
https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/discuss/135704/Detail-explanation-of-DP-solution
以上是关于算法: 买卖股票III 123. Best Time to Buy and Sell Stock III的主要内容,如果未能解决你的问题,请参考以下文章
[LeetCode] 123. Best Time to Buy and Sell Stock III 买卖股票的最佳时间 III