Studying-代码随想录训练营day41| 121.买卖股票的最佳时机、122.买卖股票的最佳时机II、123.买卖股票的最佳时机III

第41天,动态规划part08,经典题型买卖股票💪,编程语言C++

目录

121.买卖股票的最佳时机

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

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

总结


121.买卖股票的最佳时机

文档讲解:代码随想录买卖股票的最佳时机

视频讲解:手撕买卖股票的最佳时机

题目:121. 买卖股票的最佳时机 - 力扣(LeetCode)

学习:本题是买卖股票题型的第一道题,也是较为简单的一题,本题有三种解题方法。

1.暴力枚举:通过两个for循环,枚举出所有的买入卖出的可能,取其中的最大值,时间复杂度过高,容易超时:

//时间复杂度O(n^2)
//空间复杂度O(1)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //暴力枚举
        int result = 0; //保存答案
        for(int i = 0; i < prices.size(); i++) {
            for(int j = i + 1; j < prices.size(); j++) {
                result = max(result, prices[j] - prices[i]);
            }
        }
        return result;
    }
};

2.贪心算法:也可以认为是一种双指针方法,只进行一次遍历,左指针保存左边的最小值,右指针的值比左指针大则计算一次答案,比左指针小则更新左指针,最终取最大的答案。

//时间复杂度O(n)
//空间复杂度O(1)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //贪心算法,不断地找左边的最小值,和右边的数进行加减
        int result = 0; //保存答案
        int minindex = INT_MAX; //保存最小值
        for(int i = 0; i < prices.size(); i++) {
            minindex = min(minindex, prices[i]);
            result = max(prices[i] - minindex, result);
        }
        return result;
    }
};

3.动态规划:该方法不仅能解出本题,也是买卖股票这类题型的通用方法之一。从动归五部曲进行分析。

确定dp数组以及下标的含义:对于第i天来说,只会存在两种状态,1.持有该股票的状态。2.不持有该股票的状态。因此我们可以设置一个二维dp数组,dp[i][0],dp[i][1]分别表示第i天的两种状态下能够获得的最大金额。

确定递推公式:dp[i][0]表示第i天持有该股票的最大金额,因此有两种可能,一种是之前就持有了股票dp[i - 1][0];一种是之前没有持有股票,今天买入了股票(这里注意,本题只能进行一次买卖,因此在买入股票之前总金额是0)则是0 - prices[i]。

dp[i][1]同理也有两种可能,一种是之前就不持有股票dp[i - 1][1];一种是之前持有股票,今天我们给卖出了dp[i - 1][0] + prices[i]。

dp数组初始化:显然我们需要初始化的dp[0][0]和dp[0][1],对于dp[0][0]来说只能是第0天买入股票,因此dp[0][0] = prices[0]。对于dp[0][1]来说,第0天不持有,可以理解为第0天买了又卖了,也可以理解为只有一天,但无论如何总金额都为0,dp[0][1] = 0。

确定遍历顺序:由于需要前一个状态的量,因此遍历顺序为从前往后遍历。

举例dp数组:最后得到的肯定是dp[i][1],因为股票最后肯定要卖出才能得到最多的钱。

代码:

//时间复杂度O(n)
//空间复杂度O(n)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //动态规划
        //1.确定dp数组以及下标的含义
        //dp[i][0]表示第i天持有该股票的最大金额,dp[i][1]表示第i天不持有该股票的最大金额
        vector<vector<int>> dp(prices.size(),vector<int>(2,0)); 
        //2.确定递推公式
        //dp[i][0] = max(-prices[i], dp[i - 1][0]); (对i股票买还是不买情况分析)
        //dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
        //3.初始化dp数组
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        //4.确定遍历顺序,之和i-1有关
        for(int i = 1; i < prices.size(); i++) {
            dp[i][0] = max(-prices[i], dp[i - 1][0]);
            dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
        }
        return dp[prices.size() - 1][1];
    }
};

代码:由于当前状态只依赖于上一个状态,因此我们只需要维护两个结果即可:

//时间复杂度O(n)
//空间复杂度O(1)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //动态规划:滚动数组的方式
        //1.确定dp数组,以及下标含义
        //因为根据递推公式当前的值只和上一个值有关,因此我们只需要维护两个数值即可
        vector<vector<int>> dp(2,vector<int>(2,0));
        //2.确定递推公式
        //dp[1][0] = max(dp[0][0], -prices[i]);
        //dp[1][1] = max(dp[0][1], prices[i] + dp[0][0]);
        //3.初始化dp数组
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        //4.确定遍历顺序
        for(int i = 1; i < prices.size(); i++) {
            dp[1][0] = max(dp[0][0], -prices[i]);
            dp[1][1] = max(dp[0][1], prices[i] + dp[0][0]);
            dp[0] = dp[1]; //保存当前值,进入下一层循环
        }
        return dp[1][1];
    }
};

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

文档讲解:代码随想录买卖股票的最佳时机II

视频讲解:手撕买卖股票的最佳时机II

题目:122. 买卖股票的最佳时机 II - 力扣(LeetCode)

学习:本题相较于上一题只是改变了交易的条件。本题允许进行多次交易,因此相较于上一题的递推公式,只有dp[i][0]有所不同,由于可以进行多次交易,因此第i天持有股票的可能性有两种可能:1.前一天就已经持有个股票dp[i - 1][0]。2.前一天不持有股票,但前一天不持有过金额不一定为0,也有可能前面已经进行了交易,因此应该是dp[i - 1][1] - prices[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])

其他的地方没有区别,因此我们可以写出代码:

//时间复杂度O(n)
//空间复杂度O(n)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //动态规划
        //1.确定dp数组以及下标含义
        //dp[i][0]表示第i天持有股票的时候的最大金额,dp[i][1]表示第i天不持有股票的时候的最大金额
        vector<vector<int>> dp(prices.size(),vector<int>(2,0));
        //2.确定递推公式
        // dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); //分为之前就持有,和之前不持有现在持有
        // dp[i][1] = max(dp[i - 1][1], d[i - 1][0] + prices[i]); 
        //3.初始化dp数组
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        //4.确定遍历顺序
        for(int i = 1; i < prices.size(); 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 dp[prices.size() - 1][1];
    }
};

代码:由于当前状态只依赖于上一个状态,所以我们可以只维护两个结果即可,本题采用的是余数的方式来选择新的数组进行更新。

//时间复杂度O(n)
//空间复杂度O(1)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //动态规划,滚动数组的方式
        vector<vector<int>> dp(2,vector<int>(2,0));
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        //使用余数的方式选择当前数组
        for(int i = 1; i < prices.size(); i++) {
            dp[i % 2][0] = max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] - prices[i]);
            dp[i % 2][1] = max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] + prices[i]);
        }
        return dp[(prices.size() - 1) % 2][1];
    }
};

代码:甚至!由于买卖股票的特殊性,我们只会取更大的金额,因此用一维数组也能够完成

//时间复杂度O(n)
//空间复杂度O(1)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //动态规划
        vector<int>dp(2,0);
        dp[0] = -prices[0];
        dp[1] = 0;
        for(int i = 1; i < prices.size(); i++) {
            dp[0] = max(dp[0], dp[1] - prices[i]);
            dp[1] = max(dp[1], dp[0] + prices[i]);
        }
        return dp[1];
    }
};

贪心算法:当然,实际上本题我们在贪心算法中就已经解出过本题,只需要不断的收集正利润即可,更好理解。

//时间复杂度O(n)
//空间复杂度O(1)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int result = 0; //统计利润
        //贪心思路,每次只手机正利润
        for(int i = 1; i < prices.size(); i++) {
            int profit = prices[i] - prices[i - 1];
            if(profit > 0) {
                result += profit;
            }
        }
        return result;
    }
};

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

文档讲解:代码随想录买卖股票的最佳时机III

视频讲解:手撕买卖股票的最佳时机III

题目:123. 买卖股票的最佳时机 III - 力扣(LeetCode)

学习:本题再次增加难度,规定了股票交易次数为2次,增加了当前买卖的不确定性。但我们同样可以在上两题的基础上进行分析。

确定dp数组以及下标含义:对于第i天而言,它存在5种状态。1.此前包括现在什么都没操作,总金额为0;2.第一次持有股票;3.第一次不持有股票(注意第一次不持有股票,是指已经卖出了一次股票);4.第二次持有股票;5.第二次不持有股票(注意此时已经卖出了2次股票了)。

由于第一个状态总金额始终保持为0,因此我们可以设置一个二维数组来保持每天的四种状态。

vector<vector<int>> dp(prices.size(), vector<int>(4, 0));

确定递推公式:针对四种状态进行递推

  • 对于dp[i][0]表示第一次持有股票,则有两种可能:前面就已经第一次持有股票了dp[i - 1][0];前面还没有持有过股票,现在持有股票0 - prices[i]。一定要注意现在的状态是第一次持有股票,所以前面肯定还没有发生过交易。
  • 对于dp[i][1]表示第一次不持有股票,此时已经交易过一次了,也有两种可能:前面就已经不持有股票了,且交易过了一次dp[i - 1][1];前面持有一次股票还没有交易,现在交易dp[i - 1][0] + prices[i];
  • 对于dp[i][2]表示第二次持有股票,此时也是已经交易过一次的状态,有两种可能:前面已经第一次持有股票了dp[i - 1][2];前面交易过一次后还没有持有股票,现在持有股票dp[i - 1][1] + prices[i]。
  • 对于dp[i][3]表示第二次不持有股票,此时已经交易了两次了,有两种可能:前面已经发生了两次交易了dp[i - 1][3];前面交易过一次,且第二次持有股票,今天卖出dp[i - 1][2] + prices[i]。

初始化dp数组:显然我们需要初始化dp[0],也就是第0天的四种状态。dp[0][0] = -prices[0]表示第一天买入的情况;dp[0][1] = 0可以理解为第一天买入又卖出;dp[0][2] = -prices[0]表示第一天买入又卖出又买入的情况;dp[0][3] = 0可以理解为第一天买入卖出又买入卖出的情况。(这一天买入卖出看似是没有意义的,实际上还是有意义且符合题意的,例如[1,2,3]的情况,我们最大值就是第一天买入第三天卖出,而第二次就可以是第三天买入又卖出)

确定遍历顺序:由于需要前面的结果,因此本题应该从前往后进行遍历。

举例dp数组:

代码:

//时间复杂度O(n)
//空间复杂度O(n*5)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        //动态规划
        //1.确定dp数组以及下标的含义
        //dp[i]有四种状态,第一次持有,第一次不持有,第二次持有,第二次不持有(实际上还有个状态是从未操作过,为0)
        vector<vector<int>> dp(prices.size(), vector<int>(4, 0));
        //2.确定递推公式:后面给出
        //3.初始化dp数组
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        dp[0][2] = -prices[0];
        dp[0][3] = 0;
        //4.确定遍历顺序
        for(int i = 1; i < prices.size(); i++) {
            dp[i][0] = max(dp[i - 1][0], 0 - prices[i]); //第一次持有,可能是前面就持有,也可能是前面一次也没有持有,现在买入
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]); 
            dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] - prices[i]);
            dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] + prices[i]);
        }
        return dp[prices.size() - 1][3];
    }
};

总结

买卖股票问题除了以上三题以外还有三道题,重点是要分析每一天的状态。这种dp数组的设置方式,与树形dp的设置方式有相同之处,需要多加练习。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值