动态规划-股票利益最大化问题

股票利益最大化问题:

给定股票价格序列,规定(买入、卖出)的最多次数K,求最大的总利润。(一次买入和卖出为完整交易)
1.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;
    }
};

2.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;
    }
};

3.不限定买卖次数,即K= + ∞ +\infty + 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进行更新。
状态转移方程:

dp [n][k][0]=max(dp [n-1] [k] [0],dp [n-1] [k] [1]+prices[n])

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

dp [n] [k] [1]=max(dp [n-1] [k] [1],dp [n-1] [k-1] [0]-prices[n])

解释:今天(n天)手里有股票,有两种可能:
1.昨天手里就有股票,今天没有进行买入操作(k不会变),所以今天手里仍然保留股票,收益不变:dp[n-1][k][1]。
2.昨天手里没有股票,今天买入价值为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]= − ∞ -\infty 表示到第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=+ ∞ \infty 的情况,此时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.每次卖出股票后,后一天不允许买入股票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表示手里还有股票

dp [n][k][0]=max(dp[n-1][k][0],dp[n-1][k-1][1]+price[n])

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

dp [n][k][1]=max(dp[n-1][k][1],dp[n-1][k][0]-price[n])

解释:今天(n天)手里有股票,有两种可能:
1.昨天手里就有股票,今天没有进行买入操作(k不会变),所以今天手里仍然保留股票,收益不变:dp[n-1][k][1]。
2.昨天手里没有股票,今天买入价值为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];
    }
};

参考:
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/

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值