第41天,动态规划part08,经典题型买卖股票💪,编程语言C++
目录
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的设置方式有相同之处,需要多加练习。