买卖股票问题

121. 买卖股票的最佳时机I

121. 买卖股票的最佳时机

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。

注意:你不能在买入股票前卖出股票。

示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

方法1:一次遍历
在这里插入图片描述
我们只需要遍历价格数组一遍,记录历史最低点,然后在每一天考虑这么一个问题:如果我是在历史最低点买进的,那么我今天卖出能赚多少钱?当考虑完所有天数之时,我们就得到了最好的答案。

//一次遍历,更新当前位置i前面部分的最小值min,同时判断是否更新全局最大值
    int maxProfit(vector<int>& prices) {
       int N=prices.size();
       if(N<2) return 0;
       int Min=prices[0];
       int Profit=INT_MIN;
       for(int i=1;i<N;i++){
            if(prices[i-1]<Min)
                Min=prices[i-1];
            if(prices[i]-Min>Profit)
                Profit=prices[i]-Min;
       }
       //Profit<=0;说明不能进行交易
       if(Profit<=0) return 0;
       return Profit;
    }

方法2:动态规划思想

状态定义
dp[i][k]表示第i天,状态为k时(k=0:未持有股票,k=1:持有股票)获得的最大利润

转移方程:

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + price[i])
dp[i][1]表示前 i天,持有股票状态下的最大利润


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

而我们需要的答案就是,
前 n天没有持有股票状态下的最大利润:dp[n][0]

你可能注意到,
第二个状态转移方程,正常来讲应该写成:dp[i][1] = max(dp[i-1][1], dp[i][0] - price[i]),但上面 maxmax 的第二个参数却是 0 - price[i] 。
这是因为题目要求股票只能买卖一次,0表示未进行股票交易时的初始金额,而 dp[i-1][0]表示前 i - 1天未持有股票状态下的最大利润,但前 i - 1天可能完成了多次股票交易,所以不满足条件。

代码

class Solution {
public:
//dp[i][k]表示第i天,状态为k时(k=0:未持有股票,k=1:持有股票)获得的最大利润
    int maxProfit(vector<int>& prices) {
        int N=prices.size();
        if(N<2) return 0;

        vector<vector<int>> dp(N+1,vector<int>(2));
        dp[0][0]=0;
        dp[0][1]=INT_MIN;
        dp[1][0]=0;
        dp[1][1]=-prices[0];
        
        for(int i=2;i<=N;i++){
            //第i天未持有股票:
            //可能是第i-1天持有股票,然后第i天卖出
            //也可能是第i-1天未持有股票,然后第i天,啥也没干,还是未持有
            dp[i][0]=max(dp[i-1][1]+prices[i-1],dp[i-1][0]);
            //第i天持有股票:
            //可能是第i-1天持有股票,然后第i天,啥也没干,还是持有
            //也可能是第i-1天未持有股票,然后第i天买入,注意这里只能有一次交易机会,
            //所以第i天买入股票时,最大利润直接是0-prices[i-1]
            //而不是dp[i-1][0]-prices[i-1];
            dp[i][1]=max(dp[i-1][1],0-prices[i-1]);

        }
        return dp[N][0];
    }
};

122. 买卖股票的最佳时机 II

122. 买卖股票的最佳时机 II

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。

示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

方法1:峰谷法
在这里插入图片描述
求出连续的峰和谷的差值,然后求和
T o t a l P r o f i t = s u m ( h e i g h t ( p e a k i ) − h e i g h t ( v a l l e y i ) ) TotalProfit=sum(height(peak_i)-height(valley_i)) TotalProfit=sum(height(peaki)height(valleyi))

int maxProfit(vector<int> prices){
    int i=0;
    int valley=prices[0];
    int peak=prices[0];
    int maxprofit=0;
    while(i<prices.size()-1){
        while(i<prices.size()-1&&prices[i]>=prices[i+1])
            i++;
        valley=prices[i];
        while(i<prices.size()-1&&prices[i]<=prices[i+1])
            i++;
        peak=prices[i];
        
        maxprofit+=peak-valley;
    }
    return maxprofit;
}

方法2:方法1的改进
我们可以简单地继续在斜坡上爬升并持续增加从连续交易中获得的利润,而不是在谷之后寻找每个峰值。

例如[1, 7, 2, 3, 6, 7, 6, 7] 与此数组对应的图形是:
在这里插入图片描述
从上图中,我们可以观察到 A+B+C 的和等于差值 D 所对应的连续峰和谷的高度之差。

int maxProfit(vector<int> prices){
    int maxprofit=0;
    for(int i=1;i<prices.size();i++){
        if(prices[i]>prices[i-1])
            maxprofit+=prices[i]-prices[i-1];
    }
    return maxprofit;
}

方法3:暴力搜索

来自:
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/solution/tan-xin-suan-fa-by-liweiwei1419-2/

根据题意:因为不限制交易次数,在每一天,我就可以根据当前是否持有股票选择相应的操作。“暴力搜索” 也叫 “回溯搜索”、“回溯法”,首先画出树形图。
在这里插入图片描述

class Solution {
public:
//暴力回溯
    int res=0;
/*
prices:股票价格数组
depth:决策树深度
status:当前状态, 0:当前未持有股票;1:当前持有股票
profit:当达当前深度时的收入
*/
    void trackback(vector<int>& prices,int depth,int status,int profit){
        if(depth==prices.size()-1){
            //最大利润肯定出现在最后未持有股票的情况下
            if(status==0&&profit>res)
                res=profit;
            return ;
        }
        
        //status==0当前未持有股票
        if(status==0){
            //可以买入股票,花费prices[depth+1],收入变为profit-prices[depth+1]
            trackback(prices,depth+1,1,profit-prices[depth+1]);
            //可以不买入股票,不花钱,收入还是profit
            trackback(prices,depth+1,0,profit);
        }
        //status==1当前持有股票
        else{
            //可以卖出股票,获得prices[depth+1],收入变为profit+prices[depth+1]
            trackback(prices,depth+1,0,profit+prices[depth+1]);
            //可以不卖出股票,收入还是profit
            trackback(prices,depth+1,1,profit);
        }
    }

    int maxProfit(vector<int>& prices) {
        int N=prices.size();
        if(N<2) return 0;
        
        int profit=0;
        
        //第一天可以不买入股票
        trackback(prices,0,0,profit);
        //第一天也可以买入股票
        trackback(prices,0,1,profit-prices[0]);

        return res;
    }
};

暴力回溯会超时
在这里插入图片描述

方法4:动态规划
来自:
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/solution/tan-xin-suan-fa-by-liweiwei1419-2/

第 1 步:定义状态
状态 dp[i][j] 定义如下
第一维 i 表示索引为 i 的那一天(具有前缀性质,即考虑了之前天数的收益)能获得的最大利润;
第二维 j 表示索引为 i 的那一天是持有股票,还是持有现金。这里 0 表示持有现金(cash),1 表示持有股票(stock)。

第 2 步:思考状态转移方程
状态从持有现金(cash)开始,到最后一天我们关心的状态依然是持有现金(cash);
每一天状态可以转移,也可以不动。状态转移用下图表示:
在这里插入图片描述
(状态转移方程写在代码中)

说明:
因为不限制交易次数,除了最后一天,每一天的状态可能不变化,也可能转移;
写代码的时候,可以不用对最后一天单独处理,输出最后一天,状态为 0 的时候的值即可。

第 3 步:确定起始
起始的时候:
如果什么都不做,dp[0][0] = 0;
如果买入股票,当前收益是负数,即 dp[0][1] = -prices[i];

第 4 步:确定终止
终止的时候,上面也分析了,输出 dp[len - 1][0],因为一定有 dp[len - 1][0] > dp[len - 1][1]。

int maxProfit(vector<int>& prices) {
        int N=prices.size();
        if(N<2) return 0;
        
        vector<vector<int>> dp(N,vector<int>(2));
        //base case
        dp[0][0]=0;
        dp[0][1]=-prices[0];
        //填表
        for(int i=1;i<N;i++){
            dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i]);
            dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i]);
        }
        //返回结果
        return max(dp[N-1][0],dp[N-1][1]);
    }

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

解法来自:
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/

https://leetcode-cn.com/circle/article/qiAgHn/

123. 买卖股票的最佳时机 III
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔交易

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:
输入: [3,3,5,0,0,3,1,4]
输出: 6
解释: 在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。

示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。

注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

动态规划思路:
我们可以对「状态」进行穷举。我们具体到每一天,看看总共有几种可能的「状态」,再找出每个「状态」对应的「选择」。我们要穷举所有「状态」,穷举的目的是根据对应的「选择」更新状态。

for 状态1 in 状态1的所有取值:
    for 状态2 in 状态2的所有取值:
        for ...
            dp[状态1][状态2][...] = 择优(选择1,选择2...)

每天都有三种「选择」:买入、卖出、无操作,我们用 buy, sell, rest 表示这三种选择。但问题是,并不是每天都可以任意选择这三种选择的,因为 sell 必须在 buy 之后,buy 必须在 sell 之后。那么 rest 操作还应该分两种状态,一种是 buy 之后的 rest(持有了股票),一种是 sell 之后的 rest(没有持有股票)。而且别忘了,我们还有交易次数 k 的限制,就是说你 buy 还只能在 k > 0 的前提下操作。

这个问题的「状态」有三个,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(即之前说的 rest 的状态,我们不妨用 1 表示持有,0 表示没有持有)。然后我们用一个三维数组就可以装下这几种状态的全部组合:

dp[i][k][0 or 1]
0 <= i <= n-1, 1 <= k <= K
n 为天数,大 K 为最多交易数
此问题共 n × K × 2 种状态,全部穷举就能搞定。

for 0 <= i < n:
    for 1 <= k <= K:
        for s in {0, 1}:
            dp[i][k][s] = max(buy, sell, rest)

状态转移图
在这里插入图片描述
根据这个图,我们来写一下状态转移方程:

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
              max(   选择 rest  ,             选择 sell      )

解释:今天我没有持有股票,有两种可能:
要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。

dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
              max(   选择 rest  ,           选择 buy         )

解释:今天我持有着股票,有两种可能:
要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。

如果 buy,就要从利润中减去 prices[i],如果 sell,就要给利润增加 prices[i]。今天的最大利润就是这两种可能选择中较大的那个。而且注意 k 的限制,我们在选择 buy 的时候,把 k 减小了 1,很好理解吧,当然你也可以在 sell 的时候减 1,一样的。 (只能在买入的时候减1才不会出错)

base case
关于这里的负无穷,得仔细想想为啥?

dp[-1][k][0] = 0
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。
dp[-1][k][1] = -infinity
解释:还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能。
dp[i][0][0] = 0
解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0 。
dp[i][0][1] = -infinity
解释:不允许交易的情况下,是不可能持有股票的,用负无穷表示这种不可能。

一种写法:

class Solution {
public:
//动态规划
//dp[i][j][k]:表示第i天,最多还能完成j笔交易,状态为k的时候的最大利润
//k:0表示未持有股票,1表示持有股票
//状态转移:
//dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j][1]+prices[i-1])
//dp[i][j][1]=max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i-1])

//base case
//dp[0][j][0]=0
//dp[0][j][1]=INT_MIN 用负无穷表示不可能

    int maxProfit(vector<int>& prices) {
        int N=prices.size();
        if(N<2) return 0;
        int j=2;
        
        //三维dp
        vector<vector<vector<int>>> dp(N+1,vector<vector<int>>(j+1,vector<int>(2)));

        for(int i=0;i<=N;i++){
            for(int j=1;j<=2;j++){
                if(i==0){
                    //处理base case
                    dp[i][j][0]=0;
                    dp[i][j][1]=INT_MIN;
                }else if(j==0){
                    dp[i][0][0]=0;
                    dp[i][0][1]=INT_MIN;
                }else{
                    //关于在第i天买入股票时dp[i-1][j-1][0]-prices[i-1],
             //因为必须留一次给第i天,才能买入,所以第i-1天未持有股票时的可交易次数为j-1次
                    dp[i][j][1]=max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i-1]); 
                                      
                    dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j][1]+prices[i-1]);
                }     
            }
        }
        return dp[N][2][0];
    }
};

另一种写法

class Solution {
public:
//动态规划
//dp[i][j][k]:表示第i天,最多还能完成j笔交易,状态为k的时候的最大利润
//k:0表示未持有股票,1表示持有股票
//状态转移:
//dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j][1]+prices[i-1])
//dp[i][j][1]=max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i-1])

//base case
//dp[0][j][0]=0
//dp[0][j][1]=INT_MIN 用负无穷表示不可能

    int maxProfit(vector<int>& prices) {
        int N=prices.size();
        if(N<2) return 0;
        int j=2;
        
        //三维dp
        vector<vector<vector<int>>> dp(N+1,vector<vector<int>>(j+1,vector<int>(2)));
        //处理base case
        dp[0][1][0]=0;
        dp[0][1][1]=INT_MIN;
        dp[0][2][0]=0;
        dp[0][2][1]=INT_MIN;

        for(int i=1;i<=N;i++){
            //dp[i][0][0]已经是0了,不用管
            
            //关于这里的j-1,在买入股票的时候j减1
            dp[i][1][1]=max(dp[i-1][1][1],dp[i-1][0][0]-prices[i-1]);                   
            dp[i][1][0]=max(dp[i-1][1][0],dp[i-1][1][1]+prices[i-1]);

            dp[i][2][1]=max(dp[i-1][2][1],dp[i-1][1][0]-prices[i-1]);                   
            dp[i][2][0]=max(dp[i-1][2][0],dp[i-1][2][1]+prices[i-1]);
                     
        }

        return dp[N][2][0];
    }
};

再进行状态压缩,降低空间复杂度

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

        int dp10=0;
        int dp11=INT_MIN;
        int dp20=0;
        int dp21=INT_MIN;

        for(int i=1;i<=N;i++){
            dp10=max(dp10,dp11+prices[i-1]);
            dp11=max(dp11,-prices[i-1]);  
            
            dp20=max(dp20,dp21+prices[i-1]);
            dp21=max(dp21,dp10-prices[i-1]);                                    
        }
        return dp20;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值