问题定义

给定股票价格序列,规定(买入、卖出)的最多次数K,求最大的总利润。(一次买入和卖出为完整交易)

K=1

Leetcode121记录遍历到第i天的时候之前的股票价格最小值,那么如果当天卖出,能取得最大利润肯定是当前股票价格减去之前最低的股票价格最后取遍历的最大值。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int max_profit = 0;
        int pre_min = INT_MAX;
        for(int i=0;i<prices.size();i++)
        {
            max_profit=max(max_profit,prices[i]-pre_min);
            pre_min = min(pre_min,prices[i]);
        }
        return max_profit;
    }
};

K=2

LeetCode123 相当于找两个利润最大的价格区间,并且这两个区间是不重叠的,也就是说这两个区间左右一个,那么如果在第i天把区间分成两部分,所以借用k=1的思路,每部分分别求各自的最大利益,然后遍历每天,求左右两部分最大的两个利益,最后去最大值即可。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size()<=0)
            return 0;
        int len=prices.size();
        int *dp_left=new int[len];
        int *dp_right=new int[len];
        dp_left[0]=0;
        int min_val=INT_MAX,max_profit=0;
        for(int i=0;i<len;i++)
        {
             max_profit=dp_left[i]=max(max_profit,prices[i]-min_val);
             min_val=min(min_val,prices[i]);
        }
        max_profit=0;
        int high_val=-1;
        for(int i=len-1;i>=0;i--)
        {
             max_profit=dp_right[i]=max(max_profit,high_val-prices[i]);
             high_val=max(high_val,prices[i]);
        }
        max_profit=0;
        for(int i=0;i<len;i++)
        {
            max_profit=max(max_profit,dp_left[i]+dp_right[i]);
        }
        return max_profit;
    }
};

不限定买卖次数-即K=inf

Leetcode122 因为不限制买卖次数,则此时每一个上升区间的两端,都分别可以进行买入和卖出,只需要统计每个上升区间的落差就行。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //寻找连续的波峰和波谷即可,不跨越任何一个波峰
        if(prices.size()<=0)
            return 0;
        int maxProfit=0;
        int valley=prices[0];
        int peak=prices[0];
        int i=0,len=prices.size()-1;
        while(i<len)
        {
            //下坡
            while(i<len && prices[i+1]<=prices[i])
                i++;
            valley = prices[i]; 
            //上破
            while(i<len && prices[i+1]>=prices[i])
                i++;
            peak = prices[i]; 
            maxProfit += peak-valley;
        }
        return maxProfit;
    }
};

简单形式:不需要显示的求出每个上升区间的落差,只要是上升的,那么在该区间内用后一天价格减去当天价格,对所有差求和就是区间的落差。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //和寻找连续的波峰波谷是同样的思路,但是不用具体找到确切的波峰和波谷的位置
        if(prices.size()<=0)
            return 0;
        int maxProfit=0;
        int i=0,len=prices.size()-1;
        while(i<len)
        {
            //只要找到上升的路径,就不断累加
            if(prices[i+1]>=prices[i])
                maxProfit += prices[i+1]-prices[i];
            i++;
        }
        return maxProfit;
    }
};

大杀器-通用解法

使用动态规划的思想: dp[n][k][flag]:表示在第n天时,最多买入k次股票,且目前手里持有股票的状态是flag。其中天数从0开始, flag=0表示目前手里没有股票,flag=1表示手里还有股票,其中在股票买入的时候,对k进行更新。 状态转移方程:

上式解释:今天(n天)手里没有股票,且最多买入了k次股票,有两种可能: 1.昨天手里就没有股票,也今天没有进行买卖操作(k不会变),所以今天手里还是没有股票,收益不变,仍为dp[n-1][k][0] 2.昨天手里有股票,今天把手里价值为price[n]的股票卖了(k也不会变),收益变成dp[n-1][k][1]+price[n] (买入股票时才修改k) 上式解释:今天(n天)手里有股票,有两种可能:

  • 昨天手里就有股票今天没有进行买入操作(k不变),所以今天手里仍然保留股票,收益不变:dp[n-1][k][1]。
  • 昨天手里没有股票,今天买入价值为price[n]的股票(k发生改变),收益变成dp[n-1][k-1][0]-price[n]。(以买入股票作为一个交易结束,所以k-1)

初始化:

  • dp[i][0][0]=0表示到第i天,最多使用了0次交易(就是说并未买入任何股票),那肯定就是0
  • dp[i][0][1]=表示到第i天,并未买入任何股票的情况下,手里持有股票,这是肯定不正确的,置为负无穷
  • dp[0][k][0]=0表示第0天时,最多使用了k次交易(即买入了k次股票,其实最多也就买入了一次)且手里没有股票,肯定是0
  • dp[0][k][1]=-prices[0] 表示第0天时,最多使用了k次交易(即买入了k次股票,其实最多也就买入了一次)且手里已经有股票了,表明在在第一天已经买入股票了。(此时k必须大于1)

PS:对于以卖出时候更新k的解法,读者可自行分析,参考件本文最下方;

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size()<=0)
            return 0;
        int len=prices.size();
        int K_max=2;
        vector<vector<vector<int>>> dp(len,vector<vector<int>>(K_max+1,vector<int>(2)));

        //K为任意正整数的通用解法-状态机+三维DP(买入股票时,k发生改变)
        for(int i=0;i<len;i++)
        {
            dp[i][0][0]=0;
            dp[i][0][1]=-INT_MAX;
        }
        for(int k=K_max;k>=1;k--)
        {
            dp[0][k][0]=0;
            dp[0][k][1]=-prices[0];
        }
        //接下来对第0天之后的情况使用状态转移方程计算即可
        for(int n=1;n<len;n++)
        {
            for(int k=K_max;k>=1;k--)
            {
                dp[n][k][0]=max(dp[n-1][k][0],dp[n-1][k][1]+prices[n]);
                dp[n][k][1]=max(dp[n-1][k][1],dp[n-1][k-1][0]-prices[n]);
            } 
        }
        //在最后一天(len-1)最多使用K_max次交易,肯定是把手里股票卖光收益更高(不然留着手里发霉啊?)
        //所以是dp[len-1][K_max][0],而非dp[len-1][K_max][1]
        //其次,最多使用K_max次交易交易肯定是包括最多使用K_max-1次,K_max-2次交易的情况。
        return dp[len-1][K_max][0];
    }
};

ps:对于 K=+的情况,此时K已经毫无意义,可以直删除该维度,代码精简如下:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size()<=0)
            return 0;
        int len=prices.size();
        vector<vector<int>> dp(len,vector<int>(2));
        for(int i=0;i<len;i++)
        {
            dp[i][0]=0;    
            dp[i][1]=-INT_MAX;
        }
        dp[0][1]=-prices[0];
        for(int n=1;n<len;n++)
        {
            dp[n][0]=max(dp[n-1][0],dp[n-1][1]+prices[n]);
            dp[n][1]=max(dp[n-1][1],dp[n-1][0]-prices[n]);
        }
        return dp[len-1][0];
    }
};

此外,对于通用的形式,可以看见循环中,第一维度的变化是步长为1递减的,所以可以进一步对通用形式进行空间优化。不然在总天数和k很大的时候无法通过LeetCode188

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int N=prices.size();
        if(N<=1) return 0;
        if(k>N/2)
            k=N/2+1;
        
        //套用通用解法模板(买入时更新k)
        vector<vector<int>> dp(k+1,vector<int>(2));
        for(int m=0;m<=k;m++)
        {
            dp[m][0]=0;
            dp[m][1]=-prices[0];
        }
        dp[0][1]=-INT_MAX;
        //因为n都是每次步长为1递减的,所以为了优化内存,可以把三维dp变为二维dp
        for(int n=1;n<N;n++)
        {
            for(int m=k;m>=1;m--)
            {
               dp[m][0]=max(dp[m][0],dp[m][1]+prices[n]);   
               dp[m][1]=max(dp[m][1],dp[m-1][0]-prices[n]); 
            }
        }
        return dp[k][0];
    }
};

延伸1-交手续费

如每次完整的交易需要交手续费(Leetcode714),同样可以使用通用解法:

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int len=prices.size();
        if(len<=1) return 0;
        vector<vector<int>> dp(len,vector<int>(2));
        for(int i=0;i<len;i++)
        {
            dp[i][0]=0;
            dp[i][1]=-INT_MAX;
        }
        //规定在卖出股票的时候扣手续费,所以第一天手里如果有股票,不需要扣手续费
        //ps:也可以规定在买入股票的时候扣手续费,所以第一天手里如果有股票,需要扣手续费:
        //即变成dp[0][1]=-prices[0];
        dp[0][1]=-prices[0];
        for(int i=1;i<len;i++)
        {
            //每次完整的交易才需要交一个手续费,所以是在卖出股票的时候交手续费
            dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i]-fee);
            dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i]); //如果规定买入时扣手续费,那么在这里减去fee
        }
        return dp[len-1][0];
    }
};

延伸2-后一天不允许买入股票

延伸2.每次卖出股票后,后一天不允许买入股票LeetCode309

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size()<=1)
            return 0;
        int len=prices.size();
        vector<vector<int>> dp(len,vector<int>(2));

        //第0天手里没有股票和有股票,收益分别是0和-prices[0]
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        //第1天手里没有股票的两种情况:1.根本没买过 2.在第0天买了,第二天卖了
        //第1天手里有股票的两种情况:1.第0天买的 2.在第1天买的
        dp[1][0] = max(0,prices[1]-prices[0]);
        dp[1][1] = max(-prices[0],-prices[1]);
        for(int n=2;n<len;n++)
        {
            dp[n][0]=max(dp[n-1][0],dp[n-1][1]+prices[n]);
            //当天手里有股票,可能昨天就有但是没进行任何操作,或者前天卖掉股票后手里没股票,然后昨天被冻结了
            dp[n][1]=max(dp[n-1][1],dp[n-2][0]-prices[n]);
        }
        return dp[len-1][0];
    }
};

拓展-以卖出时候更新k值

dp[n][k][flag]:表示在第n天时,最多卖出了k次股票,目前手里持有股票的状态是flag, flag=0表示目前手里没有股票,flag=1表示手里还有股票
上式解释:今天(n天)手里没有股票,且最多卖出了k次股票,有两种可能:

  • 昨天手里就没有股票,且今天没有进行买卖操作(k不会变),所以今天手里还是没有股票,收益不变,仍为dp[n-1][k][0]
  • 昨天手里有股票,今天把手里价值为price[n]的股票卖了(k会发生变化),收益变成dp[n-1][k][1]+price[n]

上式解释:今天(n天)手里有股票,有两种可能:

  • 昨天手里有股票今天没有进行买入操作(k不变),所以今天手里仍然保留股票,收益不变:dp[n-1][k][1]。
  • 昨天手里没有股票今天买入价值为price[n]的股票(k不变),收益变成dp[n-1][k-1][0]-price[n]。

初始状态:

  • dp[i][0][0]=0表示到第i天,最多使用了0次交易(就是说并未卖出任何股票),那肯定就是0
  • dp[i] [0] [1]=max{-price[z]} (其中z=0,1,2...i) 表示到第i天,并未卖出任何股票的情况下,手里持有股票,这说明买入了一次股票,那么有可能其实在第0天第i天之间任何一天买入的股票,为了保证dp[i][0][1]尽量最大(因为本身问题就是一个最大化的问题,所以令其该天及之前之前最小价格的负数)。
  • dp[0][k][0]=0表示第0天时,最多使用了k次交易(即卖出了k次股票,其实最多就买入一次)且手里没有股票,肯定是0(有可能第一天根本就没买,也有可能当天买当天卖,反正都是0)
  • dp[0][k][1]=-prices[0] 表示第0天时,最多使用了k次交易(即卖出了k次股票,其实最多也就卖出了一次)且手里已经有股票了,说明在卖出股票后又买了一次股票,收益肯定就是-prices[0] (由上面的分析可知,这里k等于0也是一样的) 代码如下:
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size()<=0)
            return 0;
        int len=prices.size();
        int K_max=2;
        vector<vector<vector<int>>> dp(len,vector<vector<int>>(K_max+1,vector<int>(2)));
        int min_val=INT_MAX;
        for(int i=0;i<len;i++)
        {
            min_val=min(min_val,prices[i]);
            dp[i][0][0]=0;
            dp[i][0][1]=-min_val;
        }
        for(int k=K_max;k>=1;k--)
        {
            dp[0][k][0]=0;
            dp[0][k][1]=-prices[0];
        }
        //接下来对第0天之后的情况使用状态转移方程计算即可
        for(int n=1;n<len;n++)
        {
            for(int k=K_max;k>=1;k--)
            {
                dp[n][k][0]=max(dp[n-1][k][0],dp[n-1][k-1][1]+prices[n]);
                dp[n][k][1]=max(dp[n-1][k][1],dp[n-1][k][0]-prices[n]);
            } 
        }
        return dp[len-1][K_max][0];
    }
};

Reference

https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/solution/yi-ge-tong-yong-fang-fa-tuan-mie-6-dao-gu-piao-wen/