前言
在本文之前,纠正了回溯章节中去重方式和排序之间的关系,可以去:记录 八十【491.递增子序列及去重方式——排序关系】中回顾。
贪心章节第5篇。动态规划第11篇。记录 八十八【122.买卖股票的最佳时机 II】
一、题目阅读
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1:
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。
最大总利润为 4 + 3 = 7 。
示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
最大总利润为 4 。
示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0。
提示:
1 <= prices.length <= 3 * 10^4
0 <= prices[i] <= 10^4
二、尝试实现
2.1 分析题目,确定方法
-
题目要求最大利润。虽然可以同一天购买,同一天出售,但是这个利润是0。所以先不考虑同一天买同一天卖这种情况;
-
分析示例,确定如何获得最大值:启发——记录 七十九【376. 摆动序列】,所以才想到改成折线图看看。
-
所以:贪心算法可以解决。
- 局部最优:每个单调递增坡的最低点买入,最高点卖出;一个递增坡获得最大利润
- 全局最优:所有的递增坡获得整体的最大利润。
2.2 贪心思路实现
- 沿用记录 七十九【376. 摆动序列】的思路和细节进行处理;用pridiff和curdiff表示递增还是递减。
- 定义 int pay = 0;代表股票买入的价格。int sale = 0;表示股票卖出的价格。
- 以示例1,较为规律的序列:
-
if(pridiff < 0 && curdiff > 0) ,代表一个单调递增坡的起点,应该买入。类似节点1,节点3;
-
if(pridiff > 0 && curdiff < 0),代表一个单调递增坡的终点,应该卖出。类似节点5,节点6;
-
for循环:从0开始到prices.size()-1结束;
-
pridiff初始为0;curdiff = prices[i+1]-prices[i];。
-
同时result代表全局的最大利润,应该求和每一个单调递增坡的最大利润。result += sale-pay;
-
所以这种情况下:代码如下——
class Solution { public: int maxProfit(vector<int>& prices) { int result = 0; int pridiff = 0; int curdiff = 0; int pay = 0;//购入的价格 int sale = 0;//出售的价格 for(int i = 0;i < prices.size()-1;i++){ curdiff = prices[i+1]-prices[i]; if(pridiff < 0 && curdiff > 0) { pay = prices[i]; } if(pridiff > 0 && curdiff < 0){ sale = prices[i]; result += sale-pay; //求和每次买卖的价格 } pridiff = curdiff; } return result; } };
- 补充情况一:起点和终点如何处理:
- 在记录 七十九【376. 摆动序列】起点构造了一个平坡,pridiff = 0。在这里起点应该构造一个下降的坡。理由如下:
- 终点处理:扩展for循环到prices.size()-1停止。终止的curdiff = -1,表示终点是一个单调坡的最高点。理由如下:
- 所以:起点和重点的处理,使得始终单调递增(示例2)和始终单调递减(示例3)得以解决,所以代码更改:
- pridiff = -1;初始化。//构造起点的下降坡
- for循环范围到i < prices.size();
- if(i == prices.size()-1) curdiff = -1;//构造终点的下降坡
class Solution { public: int maxProfit(vector<int>& prices) { int result = 0; int pridiff = -1;//构造起点的下降坡 int curdiff = 0; int pay = 0;//购入的价格 int sale = 0;//出售的价格 for(int i = 0;i < prices.size();i++){ if(i == prices.size()-1) { curdiff = -1;//构造终点的下降坡 }else{ curdiff = prices[i+1]-prices[i]; } if(pridiff < 0 && curdiff > 0) { pay = prices[i]; } if(pridiff > 0 && curdiff < 0){ sale = prices[i]; result += sale-pay; //求和每次买卖的价格 } pridiff = curdiff; } return result; } };
- 补充情况二:如果遇到平坡怎么办?以平坡的最后一个元素作为买/卖的节点。
-
所以两个pridiff和curdiff的判断条件改成:
-
if(pridiff <= 0 && curdiff > 0) 和 if(pridiff >= 0 && curdiff < 0)
所以代码改变:class Solution { public: int maxProfit(vector<int>& prices) { int result = 0; int pridiff = -1;//构造起点的下降坡 int curdiff = 0; int pay = 0;//购入的价格 int sale = 0;//出售的价格 for(int i = 0;i < prices.size();i++){ if(i == prices.size()-1) { curdiff = -1;//构造终点的下降坡 }else{ curdiff = prices[i+1]-prices[i]; } if(pridiff <= 0 && curdiff > 0) { pay = prices[i]; } if(pridiff >= 0 && curdiff < 0){ sale = prices[i]; result += sale-pay; //求和每次买卖的价格 } pridiff = curdiff; } return result; } };
- 补充情况三:单调坡中有平坡怎么办?所以pridiff = curdiff;不应该每个节点都更新。
-
更新pay时,更新pridiff;更新sale时,更新pridiff。
-
所以pridiff = curdiff;放到两个if条件里面。
代码更新:class Solution { public: int maxProfit(vector<int>& prices) { int result = 0; int pridiff = -1; int curdiff = 0; int pay = 0;//购入的价格 int sale = 0;//出售的价格 for(int i = 0;i < prices.size();i++){ if(i == prices.size()-1) { curdiff = -1; }else{ curdiff = prices[i+1]-prices[i]; } if(pridiff <= 0 && curdiff > 0) { pay = prices[i]; pridiff = curdiff; } if(pridiff >= 0 && curdiff < 0){ sale = prices[i]; result += sale-pay; pridiff = curdiff; } } return result; } };
2.3 代码实现【贪心算法】
class Solution {
public:
int maxProfit(vector<int>& prices) {
int result = 0;
int pridiff = -1;
int curdiff = 0;
int pay = 0;//购入的价格
int sale = 0;//出售的价格
for(int i = 0;i < prices.size();i++){
if(i == prices.size()-1) {
curdiff = -1;
}else{
curdiff = prices[i+1]-prices[i];
}
if(pridiff <= 0 && curdiff > 0) {
pay = prices[i];
pridiff = curdiff;
}
if(pridiff >= 0 && curdiff < 0){
sale = prices[i];
result += sale-pay;
pridiff = curdiff;
}
}
return result;
}
};
三、参考学习
【122.买卖股票的最佳时机 II】 参考学习链接
3.1 【122.买卖股票的最佳时机 II】贪心思路
- 回到题目中:“在每一天,你可以决定是否购买 和/或出售股票。”——含义是:当天可以只买入或者只卖出或者既买入又卖出。很明显:同一天买入同一天卖出没有利润,因此至少需要两天完成一个交易。所以利润可以进行拆分:
- 如何进行利润拆分?如下图:
- 总结:
- 个人思路:从记录 七十九【376. 摆动序列】中获得区间单调坡的概念,所以使用区间的利润可以找到一个最低点买入,最高点卖出。但是情况思考比较多,处理细节也繁琐。
- 参考思路:发现利润可以拆分,所以只搜集正数即可。
3.2 代码实现【贪心:参考思路】
边遍历边搜集利润中的正数。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int result = 0;
for(int i = 1;i < prices.size();i++){
result += max(prices[i]-prices[i-1],0);//只搜集正数
}
return result;
}
};
3.3 动态规划思路
3.3.1思考
- 买卖股票系列是动态规划章节内容,但是贪心算法可以帮助一些题目巧妙解决,并不代表贪心可以解决所有买卖股票系列的题目。
- 那么尝试动态规划实现:
- 定义dp数组和下标:dp[i]代表在第i天卖出股票,所能获得的最大利润是dp[i]。
- 递推公式:内层循环 j 遍历第i天之前的每一天,dp[i] = max(dp[i] , dp[j] + prices[i] - prices[j]);
- 初始化:dp[0] = 0;在第0天卖出只能第0天买入,所以利润是0.
- 遍历顺序:从递推公式知道外层for循环从前往后。
- 返回值是dp数组中什么?所以dp数组的含义设定不正确。那么该如何设定dp数组?
3.3.2 正确思路
- dp数组含义:
- dp[i][0]:在第i天持有股票得到的现金dp[i][0];
- dp[i][1]:在第i天不持有股票得到的现金dp[i][1];
- 因为最多持股一个,所以当天要么有股票(1个),要么没有股票。
- 所得现金 = 利润是一个累积值。返回值:最后一天不持有股票时所得现金,即dp[最后一个][1]。
- 递推公式:
- 求dp[i][0],在第i天持有股票,得到多少钱?
- 第i天持有股票,要么第i-1天持有股票,今天继续持有不卖出,钱不增加不减少。所得现金:dp[i-1][0]。
- 第i天持有股票,,要么第i-1天不持有股票,今天重新买入。钱在dp[i-1][1]的基础上减去买入的prices[i]。所得现金:dp[i-1][1] - prices[i]。
- 取两者的最大值。dp[i][0] = max(dp[i-1][0],dp[i-1][1] - prices[i])。
- 求dp[i][1],在第i天不持有股票,得到多少钱?
- 第i天不持有股票,要么第i-1天持有股票,今天以prices[i]卖出。钱在dp[i-1][0]基础上多了prices[i]。所得现金:dp[i-1][0] + prices[i];
- 第i天不持有股票,要么第i-1天不持有股票,今天也不买入。钱依然是第i-1天不持有股票所得的钱。所以现金:dp[i-1][1]。
- 取两者的最大值。dp[i][1] = max(dp[i-1][0] + prices[i],dp[i-1][1])。
- 初始化:
- dp[0][0] = -prices[0]。先买入,花掉了-prices[0]。
- dp[0][1] = 0;相当于不买入。钱没花没赚。
- 遍历顺序:从递推公式看,从前往后。
3.4 代码实现【动态规划】
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
//定义dp数组
vector<vector<int>> dp(len,vector<int>(2,0));
//初始化
dp[0][0] = -prices[0];
dp[0][1] = 0;
//遍历,递推公式
for(int i = 1;i <len;i++){
dp[i][0] = max(dp[i-1][0],dp[i-1][1]- prices[i]);
dp[i][1] = max(dp[i-1][0] + prices[i],dp[i-1][1]);
}
return dp[len-1][1];
}
};
总结
本文:
- 纠正回溯章节中去重方式和排序之间的关系:记录 八十【491.递增子序列及去重方式——排序关系】
- 【122.买卖股票的最佳时机 II】三种方法实现:
(欢迎指正,转载标明出处)