算法: 买卖股票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)因为我们初始化s4INT_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

123买卖股票的最佳时机III

[LeetCode] 123. 买卖股票的最佳时机 III

leetcode No.123 买卖股票的最佳时机 III

代码随想录算法训练营第五十天| 123. 买卖股票的最佳时机 III188. 买卖股票的最佳时机 IV。

LeetCode 0123.买卖股票的最佳时机III:常数空间下的动态规划+模拟