Leetcode股票问题分析(Most consistent ways of dealing with the series of stock problems)

在leetcode上所有的股票问题有:

1,121. Best Time to Buy and Sell Stock
2,122. Best Time to Buy and Sell Stock II
3,123. Best Time to Buy and Sell Stock III
4,188. Best Time to Buy and Sell Stock IV
5,309. Best Time to Buy and Sell Stock with Cooldown
6,714. Best Time to Buy and Sell Stock with Transaction Fee
对于每个问题,都有一些出色的方法。但是,大多数方法都无法确定这些问题之间的联系,因此很难找到一致的方式来处理这一系列问题。在这里,我将介绍适用于所有这些问题的最通用的解决方案,并将其专门化为上述六个问题中的每一个。

1-General cases

最基本的问题:给定一个代表每天股票价格的数组,如何决定我们可以获得的最大利润?
大多数人都可以很快想出诸如“这取决于我们在哪一天以及我们可以完成多少笔交易”之类的答案。当然,这些都是重要的因素,因为它们在问题描述中表现出来。但是,在确定最大利润时,有一个隐藏的因素并不是那么明显,但至关重要,下面将详细说明。
首先,用一些符号来阐述我们的分析。n表示股票数组的长度,i表示第i天(范围是0–n-1),k表示允许完成的最大交易数,T[i][k] 表示第i天结束时最多进行k笔交易的最大利润。所以我们可以得到基本情况:T[-1][k] = T[i][0] = 0 这表示没有股票没有交易所以利润为0(注意第一天是i=0所以i=-1是没有股票的意思)。现在,如果我们能够以某种方式将T[i][k] 与它的子问题相关,例如:T[i-1][k],T[i][k-1],T[i-1][k-1]…,所以设计一个有效的递归关系,并且可以递归解决问题。那么该如何实现呢?
最直接的方法是查看第i天采取的行为。有几种选择呢?答案是三种: 买,卖,休息。我们应该选哪一个?答案是:我们并不真正知道,但是要找出哪一个很容易。在没有其他限制的情况下,我们可以尝试每种选择,然后选择能使我们的利润最大化的选择。但是,确实有一个额外的限制,即不允许同时进行多个交易。这意味着如果我们决定在第i天买股票,那么买之前手中的股票数应该为0,如果我们决定在第i天卖出,则在我们卖出之前,手中应该恰好持有1只股票。我们手中持有的股票数量是上述隐藏的因素,它将影响第i天的操作,从而影响最大利润。
因此,之前我们定义的T[i][k] 应该被分为:T[i][k][0] 和 T[i][k][1],其中前者表示在第i天结束时,最多k笔交易且 行动后 我们手中有0只股票的最大利润,而后者表示在第i天结束时,最多k笔交易且 行动后 我们手中有1只股票的最大利润。所以,基本的递归关系可以写成:
(1)Base cases:
T[-1][k][0] = 0, T[-1][k][1] = -Infinity
T[i][0][0] = 0, T[i][0][1] = -Infinity

(2)Recurrence relations:
T[i][k][0] = max(T[i-1][k][0], T[i-1][k][1] + prices[i])
T[i][k][1] = max(T[i-1][k][1], T[i-1][k-1][0] - prices[i])

对于基本情况,T[-1][k][0] = T[i][0][0] = 0具有与以前相同的含义,而T[-1][k][1] = T[ i][0][1] = -Infinity 强调一个事实,即如果没有可用的库存或不允许进行交易,那么我们手头就不可能有1只股票。
对于递归关系中的T[i][k][0] ,在第i天采取的行动只能是休息和卖出,因为在一天结束时我们手中的股票为0。如果采取行动为休息时,则T[i-1][k][0] 是最大利润,而采取行动为卖出时,则T[i-1][k][1]+prices[i] 是最大利润。注意,由于交易是一个成对出现的行为(即买入和卖出),因此允许交易的最大数量保持不变。只有动作买会更改允许的最大交易数量。
对于递归关系中的T[i][k][1],在第i天采取的行动只能是休息和购买,因为在一天结束时我们手中只有1只股票。如果采取行动为休息时,则T[i-1][k][1] 是最大利润,而如果采取行动为买入时,则T[i-1][k-1][0]-prices[i] 是最大利润采取。注意,允许交易的最大数量减少了一个,因为在第i天进行购买使用一次交易,如上所述。
要找到最后一天结束时的最大利润,我们可以简单地遍历价格数组并根据上述重复递归关系更新T[i][k][0]和T[i][k][1] 。最终答案将是T[i][k][0](如果最终手头有0只股票,我们总是有更大的利润)。

2-Applications to specific cases

上述六个股票问题按k的值分类,k的值是允许交易的最大数量(后两个还具有其他要求,例如“冷冻期”或“交易费”)。

case1: k=1 (leetcode121)

对于这种情况,我们实际上每天都有两个未知变量:T[i][1][0]和T[i][1][1],递归关系为:
T[i][1][0] = max(T[i-1][1][0], T[i-1][1][1] + prices[i])
T[i][1][1] = max(T[i-1][1][1], T[i-1][0][0] - prices[i]) = max(T[i-1][1][1], -prices[i])

在这里,针对第二个方程利用了基本情况T[i][0][0] = 0
代码:

#include 
#include 
#include 

using namespace std;

class Solution {
public:
    int maxProfit(vector<int>& prices) {

        int n = prices.size();
        if(n <= 1)
            return 0;

        int T_i10 = 0;
        int T_i11 = INT_MIN;
        for (int i = 0; i < n; i++) {
            T_i10 = max(T_i10, T_i11+prices[i]);
            T_i11 = max(T_i11, -prices[i]);
        }

        return T_i10;

    }
};

如果我们更仔细地检查循环中的部分,则T_i11实际上只是代表直到第i天所有股票价格的负值的最大值,或者等效地是所有股票价格的最小值。至于T_i10,我们只需要决定哪个动作产生更高的利润,即卖出还是休息。如果采取卖出行动,我们买入股票的价格为T_i11,即第i天之前的最小值。如果我们想获得最大的利润,这正是我们在现实中会做的。

case2: k= +Infinity (leetcode122)

如果k为正无穷大,则k与k-1之间实际上没有任何区别,这意味着T[i-1][k-1][0] = T[i-1][k][0] 和 T[i-1][k-1][1] = T[i-1 ][k][1]。因此,我们每天仍有两个未知变量:T [i][k][0] 和 T[i][k][1],其中k = Infinity,递归关系:
T[i][k][0] = max(T[i-1][k][0], T[i-1][k][1] + prices[i])
T[i][k][1] = max(T[i-1][k][1], T[i-1][k-1][0] - prices[i]) = max(T[i-1][k][1], T[i-1][k][0] - prices[i])

代码:

#include 
#include 
#include 

using namespace std;

class Solution {
public:
    int maxProfit(vector<int>& prices) {

        int n = prices.size();
        if(n <= 1)
            return 0;

        int T_ik0 = 0;
        int T_ik1 = INT_MIN;
        for (int i = 0; i < n; i++) {
            int T_iprek0 = T_ik0;
            T_ik0 = max(T_ik0, T_ik1+prices[i]);
            T_ik1 = max(T_ik1, T_iprek0-prices[i]);
        }

        return T_ik0;
    }
};

case3: k= 2 (leetcode123)

类似于k = 1的情况,现在有四个变量而不是有两个变量:T[i][1][0],T[i][1][1],T[i][2][0],T [i][2][1],递归关系为:
T[i][2][0] = max(T[i-1][2][0], T[i-1][2][1] + prices[i])
T[i][2][1] = max(T[i-1][2][1], T[i-1][1][0] - prices[i])
T[i][1][0] = max(T[i-1][1][0], T[i-1][1][1] + prices[i])
T[i][1][1] = max(T[i-1][1][1], -prices[i])

代码:

#include 
#include 
#include 

using namespace std;

class Solution {
public:
    int maxProfit(vector<int>& prices) {

        int n = prices.size();
        if(n<=1)
            return 0;

        int T_i20 = 0;
        int T_i21 = INT_MIN;
        int T_i10 = 0;
        int T_i11 = INT_MIN;
        for (int i = 0; i < n; i++) {
            T_i20 = max(T_i20, T_i21+prices[i]);
            T_i21 = max(T_i21, T_i10-prices[i]);
            T_i10 = max(T_i10, T_i11+prices[i]);
            T_i11 = max(T_i11, -prices[i]);
        }

        return T_i20;
    }
};

case4: k is arbitrary (leetcode188)

这是最普遍的情况,每天我们都需要在一天结束时用不同的k值(对应于0或1只手中的股票)更新所有最大利润。但是,如果k超过某个临界值,我们可以做一个较小的优化,超过这个临界值,最大利润将不再取决于允许的交易数量,而是由可用股票的数量(价格数组的长度决定。临界值是什么呢?
有利可图的交易至少需要两天的时间(一天买入,另一天卖出,前提是买入价低于卖出价)。如果价格数组的长度为n,则获利交易的最大数量为n / 2 ,之后将无法进行任何有利可图的交易,这意味着最大利润将保持不变。因此,k的临界值为n / 2。如果给定的k不小于该值,即k> = n / 2,我们可以将k扩展到正无穷大,并且问题等同于case2。
代码:

#include 
#include 
#include 

using namespace std;

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {

        int n = prices.size();
        if(n<=1)
            return 0;

        if(k>=(n/2)){
            int T_ik0 = 0;
            int T_ik1 = INT_MIN;
            for (int i = 0; i < n; i++) {
                int T_iprek0 = T_ik0;
                T_ik0 = max(T_ik0, T_ik1+prices[i]);
                T_ik1 = max(T_ik1, T_iprek0-prices[i]);
            }
            return T_ik0;
        }

        vector<int> T_ik0(n+1, 0);
        vector<int> T_ik1(n+1, INT_MIN);
        for (int j = 0; j < n; j++) {
            for (int i = k; i > 0 ; i--) {
                T_ik0[i] = max(T_ik0[i], T_ik1[i]+prices[j]);
                T_ik1[i] = max(T_ik1[i], T_ik0[i-1]-prices[j]);
            }
        }

        return T_ik0[k];

    }
};

case5: k = +Infinity but with cooldown (leetcode309)

这和case2相似,但是在“冷却期”的情况下,如果在第(i-1)天卖出股票,我们将无法在第i天购买股票。因此,如果要在第i天购买,我们实际上应该使用T[i-2][k][0]代替T[i-1][k][0] 。所以递归关系是:
T[i][k][0] = max(T[i-1][k][0], T[i-1][k][1] + prices[i])
T[i][k][1] = max(T[i-1][k][1], T[i-2][k][0] - prices[i])

代码:

#include 
#include 
#include 

using namespace std;

class Solution {
public:
    int maxProfit(vector<int>& prices) {

        int n = prices.size();

        if(n <= 1)
            return 0;

        int T_ik0 = 0;
        int T_ik1 = INT_MIN;
        int T_ipreprek0 = 0;
        int T_iprek0 = 0;
        for (int i = 0; i < n; i++) {
            T_iprek0 = T_ik0;
            T_ik0 = max(T_ik0, T_ik1+prices[i]);
            T_ik1 = max(T_ik1, T_ipreprek0-prices[i]);
            T_ipreprek0 = T_iprek0;
        }

        return T_ik0;
    }
};

case6: k = +Infinity but with transaction fee (leetcode714)

由于我们有了交易费用,我们应该在每次买或卖之后减去交易费用,递归关系为:
T[i][k][0] = max(T[i-1][k][0], T[i-1][k][1] + prices[i])
T[i][k][1] = max(T[i-1][k][1], T[i-1][k][0] - prices[i] - fee)

or
T[i][k][0] = max(T[i-1][k][0], T[i-1][k][1] + prices[i] - fee)
T[i][k][1] = max(T[i-1][k][1], T[i-1][k][0] - prices[i])

代码:
对于递归关系1:

#include 
#include 
#include 

using namespace std;

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {

        int n = prices.size();
        if(n<=1)
            return 0;

        int T_ik0 = 0;
        int T_ik1 = INT_MIN;
        int T_iprek0 = 0;
        for (int i = 0; i < n; i++) {
            T_iprek0 = T_ik0;
            T_ik0 = max(T_ik0, T_ik1+prices[i]);
            T_ik1 = max(T_ik1, T_iprek0-prices[i]-fee);
        }
        return T_ik0;
    }
};

对于递归关系2:

#include 
#include 
#include 

using namespace std;

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {

        int n = prices.size();
        if(n<=1)
            return 0;

        long T_ik0 = 0;
        long T_ik1 = INT_MIN;
        long T_iprek0 = 0;
        for (int i = 0; i < n; i++) {
            T_iprek0 = T_ik0;
            T_ik0 = max(T_ik0, T_ik1+prices[i]-fee);
            T_ik1 = max(T_ik1, T_iprek0-prices[i]);
        }
        return (int)T_ik0;
    }
};

注意,递归关系2会有数溢出的情况,要用long

3-Summary

总之,最常见的股票问题可以通过三个因素来表征,即天的序数i,允许的最大交易数k和一天结束时我们手中的股票数量。上面已经展示了最大利润及其终止条件的递归关系,这使得有了O(nk)的时间复杂度和O(k)的空间复杂度

加油!

你可能感兴趣的:(Leetcode股票问题分析(Most consistent ways of dealing with the series of stock problems))