Leetocode7道买卖股票问题总结(121+122+123+188+309+901+714)
题目1----121. 买卖股票的最佳时机I:
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock/
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
解答:
限制只能买入卖出一次
DP1:
buy[i]记录截止到第i天是买入的状态的最小花费(值为负数)
1 class Solution { 2 public: 3 int maxProfit(vector<int>& prices) { 4 if(prices.size()<2){ 5 return 0; 6 } 7 int n=prices.size(),res=0; 8 vector<int> buy(n,0); 9 buy[0]=-prices[0]; 10 for(int i=1;i<n;++i){ 11 res=max(buy[i-1]+prices[i],res); 12 buy[i]=max(-prices[i],buy[i-1]); 13 } 14 return res; 15 } 16 };
严格来说这个不算dp,计算第i天的情况时,只用到了buy[i-1]的数据。。所以前面保存的数据是没有意义的。
DP2:
dp[i]记录第i天当天卖出的最大利润,则最大利润一定等于之前某天买今天卖。
i):首先可以昨天买今天卖。
ii):还可以之前某天买今天卖。dp[i-1]等于昨天之前买入,i-1天卖出的最大利润,那么i-1天不卖,改为第i天卖也可以得到一个利润。
两个利润取最大。
1 class Solution { 2 public: 3 int maxProfit(vector<int>& prices) { 4 if(prices.size()<2){ 5 return 0; 6 } 7 int n=prices.size(); 8 vector<int> dp(n,0); 9 dp[0]=0; 10 int res=0; 11 for(int i=1;i<n;++i){ 12 dp[i]=max(0,prices[i]-prices[i-1]); 13 dp[i]=max(dp[i],dp[i-1]-prices[i-1]+prices[i]); 14 res=max(res,dp[i]); 15 } 16 return res; 17 } 18 };
遍历:
和DP一样都是O(N)时间,和第一种dp一样想法,只是不用记录dp数组了。毕竟考察i的时候,只需要buy[i-1]的数据。
思路:最高利润出现在:买入为价格最低时,卖出为买入之后价格最高时。
故当更新最小值价格时,之前的最大价格要舍弃,从当前索引继续考察。
1 class Solution: 2 def maxProfit(self, prices: List[int]) -> int: 3 l=len(prices) 4 if l<2: 5 return 0 6 _min,_max=0,0 7 i=0 8 res=0 9 while i<l: 10 if prices[i]<prices[_min]: 11 _min=i 12 _max=i 13 if prices[i]>prices[_max]: 14 _max=i 15 res=max(res,prices[_max]-prices[_min]) 16 i+=1 17 return res
题目2----122. 买卖股票的最佳时机II:
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
解答:
无限制次数的买入卖出
1 class Solution { 2 public: 3 int maxProfit(vector<int>& prices) { 4 int n=prices.size(); 5 if(n<2){return 0;} 6 vector<int> buy(n,0),sell(n,0); 7 buy[0]=-prices[0]; 8 for(int i=1;i<n;++i){ 9 sell[i]=max(sell[i-1],prices[i]+buy[i-1]);//之前卖今天不卖 or 之前买今天卖 10 buy[i]=max(buy[i-1],sell[i-1]-prices[i]);//之前买今天不买 or 之前卖了今天买 11 } 12 return sell[n-1]; 13 } 14 };
题目3----123. 买卖股票的最佳时机III:
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
解答:
限制最多两次买入卖出
定义了4个dp数组,分别是
buy1[i]:第i天是第一次买入的状态
sell[i]:第i天是第一次卖出的状态
buy2、sell2同理。
由于一开始还不能第二次买和第二次卖,所以赋值为负无穷。
1 class Solution { 2 public: 3 #define inf INT_MIN 4 int maxProfit(vector<int>& prices) { 5 int n=prices.size(); 6 if(n<2){return 0;} 7 vector<int> buy1(n,0),sell1(n,0),buy2(n,inf),sell2(n,inf); 8 buy1[0]=-prices[0]; 9 for(int i=1;i<n;++i){ 11 sell1[i]=max(prices[i]+buy1[i-1],sell1[i-1]); 12 buy1[i]=max(-prices[i],buy1[i-1]); 13 sell2[i]=max(buy2[i-1]+prices[i],sell2[i-1]); 14 buy2[i]=max(buy2[i-1],sell1[i-1]-prices[i]); 16 } 17 return max(0,max(sell2[n-1],sell1[n-1])); 18 } 19 };
方法2:先计算1次买卖最大的利润sell1[i]。再计算从第i天开始再买卖一次最大的利润sell2[i]。
第一次买卖是从前向后求(因为左侧是固定的),第二次买卖是从后向前求(因为右侧是固定的)
1 class Solution { 2 public: 3 #define inf INT_MIN 4 int maxProfit(vector<int>& prices) { 5 int n=prices.size(); 6 if(n<2){return 0;} 7 vector<int> sell1(n,0),sell2(n,0); 8 int min_buy=-prices[0]; 9 for(int i=1;i<n;++i){ 10 sell1[i]=max(sell1[i-1],prices[i]+min_buy); 11 min_buy=max(min_buy,-prices[i]); 12 } 13 int res=max(0,sell1[n-1]); 14 int max_sell=prices[n-1]; 15 for(int i=n-2;i>=0;--i){ 16 sell2[i]=max(sell2[i+1],max_sell-prices[i]); 17 max_sell=max(max_sell,prices[i]); 18 res=max(res,sell1[i]+sell2[i]); 19 } 20 return res; 21 } 22 };
之前写过的python版本,以供参考:
1 class Solution: 2 def maxProfit(self, prices) -> int: 3 l=len(prices) 4 if l<2: 5 return 0 6 res=0 7 #先算一个dp1数组 8 #dp1[i]表示截止到第i-1天只进行一次买卖的最大利润 9 dp1=[0 for i in range(l)] 10 max_price,min_price=prices[0],prices[0] 11 for i in range(1,l): 12 dp1[i]=max(dp1[i-1],prices[i]-min_price) 13 #对于第i天来说,1.如果当天卖:最大利润即当前卖出价格 14 #减去之前的最小买入价格,2如果不卖:最大利润和前一天的 15 #最大利润相同 16 min_price=min(min_price,prices[i]) #更新当前最小买入价格 17 max_price=max(max_price,prices[i]) #更新当前最大卖出价格 18 #对于任意k,dp1[k]表示k卖出的最大利润, 19 #那么需要求剩下k+1到n-1的最大利润 20 #倒着求,因为右边界不变始终为l-1,左边界在变化 21 #dp2[i]表示从i开始到最后只进行一次买卖的最大利润 22 res=dp1[-1] 23 # print(res) 24 dp2=[0 for i in range(l)] 25 max_price=prices[-1] 26 for i in range(l-2,-1,-1): 27 dp2[i]=max(dp2[i+1],max_price-prices[i]) 28 #对于第i天,1.若当天买,则最大利润即之后的最大卖出价格减去 29 #当前买入价格,2.若当天不买,最大利润和后一天的最大利润相同 30 max_price=max(max_price,prices[i]) #更新当前最大卖出价格 31 res=max(res,dp1[i-1]+dp2[i]) if i>=1 else max(res,dp2[i]) 32 # print(dp1) 33 # print(dp2) 34 return res
后来发现其实不需要存储dp数组,只需要一个变量记录上一次的状态就行,但我懒,就不写了,反正内存大任性
题目4----188. 买卖股票的最佳时机IV:
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
这道题限制买卖次数最大为k,是一个变量。
设二维dp数组。dp[i][j]表示截止到i最多完成j笔交易的最大利润。
截止第i天,最大利润首先可以是之前的最大利润,即之前就完成了j笔交易,第i天不卖。
当然第i天也可以卖,那么本拨交易的买入最晚也得是i-1天,那么i-2天之前(包含i-2天)就必须要完成j-1笔交易。
用一个max值表示截止到前一天完成j-1笔交易、并且是待卖出状态的最大利润。
递推方程为:dp[i][j]=max(dp[i-1][j],max+prices[i])
另外每次循环中要更新max的值。
另外有用例k给的无限大,那么申请dp数组时会爆栈。需要判断一下k和价格数量的关系,如果k太大,转化为上面第2题的无限次的买卖股票问题。
前面的问题1和问题3只是这道题的特殊情况,代码直接复制过去就可以运行。
1 class Solution { 2 public: 3 int maxProfit(int k, vector<int>& prices) { 4 if(prices.empty()){ 5 return 0; 6 } 7 int n=prices.size(); 8 if(2*k<n){ 9 vector<vector<int>> dp(n,vector<int>(k+1,0)); 10 //dp[i][j]表示截止第i天完成最多j次交易得到的最大收益 11 for(int j=1;j<k+1;++j){ 12 int _max=-prices[0]; 13 for(int i=1;i<n;++i){ 14 dp[i][j]=max(dp[i-1][j],_max+prices[i]); 15 _max=max(dp[i-1][j-1]-prices[i],_max); 16 } 17 } 18 return dp.back().back(); 19 } 20 else{//k太大,相当于可以无限次交易 21 vector<int> dp(n,0); 22 //dp[i]表示截止第i+1天得到的最大收益 23 int _max=-prices[0]; 24 for(int i=1;i<n;++i){ 25 dp[i]=max(dp[i-1],_max+prices[i]); 26 _max=max(_max,dp[i]-prices[i]); 27 } 28 return dp.back(); 29 } 30 return 0; 31 } 32 };
题目5----309. 最佳买卖股票时机含冷冻期:
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
这道题多了一个冷冻期的限制:卖出之后要至少休息一天才能买入
那么理所当然的,多设立一个dp数组以表示冷冻期的状态:
1 class Solution { 2 public: 3 int maxProfit(vector<int>& prices) { 4 if(prices.size()<2){ 5 return 0; 6 } 7 int n=prices.size(); 8 vector<int> buy(n,0),sell(n,0),freeze(n,0); 9 //buy[i]、sell[i]、freeze[i]分别表示第i天 10 //买入、卖出、不买不卖的最大收益(包含买股票花的钱) 11 buy[0]=-prices[0]; 12 int res=0; 13 for(int i=1;i<n;++i){ 14 buy[i]=max(buy[i-1],freeze[i-1]-prices[i]); 15 sell[i]=max(sell[i-1],buy[i-1]+prices[i]); 16 freeze[i]=max(sell[i-1],freeze[i-1]); 17 res=max(sell[i],res); 18 } 19 return res; 20 } 21 };
也可以仿照第四题的解法,用一个max值保存截止前一天待卖出状态的最大利润,不过更新max的时候,要注意冷冻期的要求。所以_max=max(_max,dp[i-2]-prices[i]),即截止i-2天卖出的最大利润,休息一天,第i天买入。之前的题目是_max=max(_max,dp[i-1]-prices[i]),注意二者的区别,虽然只是1和2的数字不同,但却是整道题的关键
1 class Solution { 2 public: 3 int maxProfit(vector<int>& prices) { 4 int n=prices.size(); 5 if(n<2){return 0;} 6 vector<int> sell(n,0); 7 sell[1]=max(0,prices[1]-prices[0]); 8 int _max=max(-prices[0],-prices[1]); 9 for(int i=2;i<n;++i){ 10 sell[i]=max(sell[i-1],_max+prices[i]); 11 _max=max(_max,sell[i-2]-prices[i]); 12 } 13 // for(int x:sell){cout<<x<<" ";}; 14 return sell[n-1]; 15 } 16 };
题目6----901. 股票价格跨度:
链接:https://leetcode-cn.com/problems/online-stock-span/
编写一个 StockSpanner 类,它收集某些股票的每日报价,并返回该股票当日价格的跨度。
今天股票价格的跨度被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。
例如,如果未来7天股票的价格是 [100, 80, 60, 70, 60, 75, 85],那么股票跨度将是 [1, 1, 1, 2, 1, 4, 6]。
其实题目就是找今天之前最近的比今天价格大的一天,如果是从昨天顺序往前查找,时间复杂度太高。下面有两种方法可以优化时间效率。
方法1:DP,有最大、最小、最多这种字眼的一般都是DP,极个别可能是DFS。
用一个一维数组dp,dp[i]表示第i天的股票价格跨度。如果i-1天的价格小于等于第i天,那么dp[i]可以直接加上dp[i-1]的值,这是因为dp[i-1]是小于等于i-1天价格的连续天数,而且i-1天价格还不大于第i天。
拿题目中的数组举栗子,当前第5天,之前0~4天的价格:100, 80, 60, 70, 60;已经算好的dp数组:1,1,1,2,1。
那么第5天价格75,i-1=4,第4天价格60,dp[4]=1,dp[i]可以加1。下面考虑4-1=3天
此时考虑第3天的价格为70,依然小于等于75,所以dp[i]再加dp[3]=2。下面考虑3-2=1天
再考虑第1天,发现其价格80>75,则退出。
概括点说,就是利用空间换取时间,保存了每一段跳跃的天数,求某一天的结果时,不再顺序查找,而是利用之前保存的数据,每次跳一大步直到目的地。平均来说,跳跃的步数应该为1。
时间O(N),空间O(N)
代码:
1 vector<int> dp; 2 vector<int> prices; 3 class StockSpanner { 4 public: 5 StockSpanner() { 6 dp.clear(),prices.clear(); 7 prices.push_back(INT_MAX); //设置第-1天价格无穷大,这样避免之后遍历中单独判断 8 dp.push_back(1); 9 } 10 11 int next(int price) { 12 prices.push_back(price); 13 dp.push_back(1);//自己大于等于自己 14 int ori=dp.size()-1,i=ori; 15 while(prices[i-1]<=prices[ori]){//每次跳一段 16 dp[ori]+=dp[i-1]; 17 i-=dp[i-1]; 18 } 19 return dp.back(); 20 } 21 }; 22 23 /** 24 * Your StockSpanner object will be instantiated and called as such: 25 * StockSpanner* obj = new StockSpanner(); 26 * int param_1 = obj->next(price); 27 */
方法2:单调栈,这个是看题解来的,记录在一起。
其实之前也做过单调栈的题目,好像也是在leetcode上的,什么天际线的。但碍于相关题目不多,没有系统学过,做题时想不起来这个方法。
原理是这个题对于今天如果价格是100,那么今天以前的所有价格小于等于100的天的数据就不需要存储了,即数据结构中之前存储的比当前数更小的数就移除。这个性质正好符合递减栈,如果栈顶小于等于当前要push的数字,就把栈顶pop掉。直到栈顶大于当前要push的数字,再push。
还是用题目的例子:如果未来7天股票的价格是 [100, 80, 60, 70, 60, 75, 85],那么股票跨度将是 [1, 1, 1, 2, 1, 4, 6]
我们用两个同步栈(即同时push,同时pop)来做。当然设置一个新struct里面包含价格和价格跨度两个变量也可以用一个栈。。
价格栈prices,跨度栈(即保存结果的栈)kuadu。
最开始prices push无穷大,kuadu push1,这样依然可以保持递减栈性质,不影响结果,为了省去后面判断栈空的代码。。(你不push也可以)
1. 100<无穷大(下面用inf表示),push
两个栈:inf ,100 1,1
2. 80<100,push
两个栈:inf,100,80 1,1,1
3. 60<80 ,push
两个栈:inf,100,80,60 1,1,1,1
4. 70>60 ,当前跨度初始值tmp为1,因为自己也小于等于自己。然后tmp+=跨度栈的栈顶(1),1+1=2
再pop两个栈顶
两个栈:inf,100,80 1,1,1
70<80,push:价格栈push70,跨度栈push tmp
两个栈:inf,100,80,70 1,1,1,2
之后以此类推。其实只要自己纸上画一画,还是很好想明白的。
时间O(N),空间O(N)
代码:
1 class StockSpanner { 2 public: 3 stack<int> prices;//递减栈 4 stack<int> kuadu; 5 StockSpanner() { 6 prices.push(INT_MAX),kuadu.push(1); 7 } 8 9 int next(int price) { 10 int tmp=1; 11 while(prices.top()<=price){ 12 tmp+=kuadu.top(); 13 prices.pop(),kuadu.pop(); 14 } 15 prices.push(price); 16 kuadu.push(tmp); 17 return tmp; 18 } 19 }; 20 21 /** 22 * Your StockSpanner object will be instantiated and called as such: 23 * StockSpanner* obj = new StockSpanner(); 24 * int param_1 = obj->next(price); 25 */
题目7----714. 买卖股票的最佳时机加手续费:
给定一个整数数组 prices
,其中第 i
个元素代表了第 i
天的股票价格 ;非负整数 fee
代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每次交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
示例 1:
输入: prices = [1, 3, 2, 8, 4, 9], fee = 2 输出: 8 解释: 能够达到的最大利润: 在此处买入 prices[0] = 1 在此处卖出 prices[3] = 8 在此处买入 prices[4] = 4 在此处卖出 prices[5] = 9 总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
注意:
0 < prices.length <= 50000
.0 < prices[i] < 50000
.0 <= fee < 50000
.
我开始想复杂了,用了二维dp做的,代码应该是对的,但是后面的用例爆栈了。
后来看了下别人的解法,一维dp即可解决。和第二题无限次交易股票的题目相比,只是在买的时候或者卖的时候减去一个fee即可(由你自己决定)。
代码:
其中have_stock是表示当前持有股票,no_stock是当前不持有股票
1 class Solution { 2 public: 3 int maxProfit(vector<int>& prices, int fee) { 4 if(prices.size()<2){return 0;} 5 int n=prices.size(); 6 vector<int> have_stock(n,0),no_stock(n,0); 7 have_stock[0]=-prices[0]; 8 for(int i=1;i<n;++i){ 9 have_stock[i]=max(have_stock[i-1],no_stock[i-1]-prices[i]); 10 no_stock[i]=max(no_stock[i-1],have_stock[i-1]+prices[i]-fee); 11 } 12 return no_stock[n-1]; 13 } 14 };