Leetcode 121. 买卖股票的最佳时机
题目链接:121 买卖股票的最佳时机
题干:给定一个数组
prices
,它的第i
个元素prices[i]
表示一支给定股票第i
天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回
0
。
1 <= prices.length <= 105
0 <= prices[i] <= 104
思考一:贪心法。贪心思路:取最左最小值,取最右最大值,那么得到的差值就是最大利润。循环过程将每天当作最右最大值,更新最左最小值以及最大利润。
代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int low = INT_MAX;
int result = 0;
for (int i = 0; i < prices.size(); i++) {
if (prices[i] < low) low = prices[i]; //取左边最小价格
result = max(result, prices[i] - low); //比较返回从下标i位置卖票的最大利润
}
return result;
}
};
思考二:动态规划。处理逻辑:仅考虑买入股票以及卖出股票来计算利润,不考虑中间的盈亏。
- 确定dp数组(dp table)以及下标的含义
dp[i][0] 表示第i天持有股票所得最多现金 (按开始现金为0,第i天买入股票现金就是 -prices[i])
dp[i][1] 表示第i天不持有股票所得最多现金
注意:“持有” 不等于 “当天买入”, “不持有” 不等于 “当天卖出”
- 确定递推公式
如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来
- 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
- 第i天买入股票,所得现金就是买入今天的股票后所得现金即:-prices[i]
并且dp[i][0]应该选所得现金最大的,所以dp[i][0] = max(dp[i - 1][0], -prices[i]);
如果第i天不持有股票即dp[i][1], 也可以由两个状态推出来
- 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
- 第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金即:prices[i] + dp[i - 1][0]
同样dp[i][1]取最大的,所以dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
- dp数组如何初始化
由递推公式 dp[i][0] = max(dp[i - 1][0], -prices[i]); 和 dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);可以看出,基础都是要从dp[0][0]和dp[0][1]推导出来。
那么dp[0][0]表示第0天持有股票,此时的持有股票就一定是买入股票,因为不可能有前一天推出来,所以dp[0][0] -= prices[0];
而dp[0][1]表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0][1] = 0;
- 确定遍历顺序
从递推公式可以看出dp[i]都是由dp[i - 1]推导出来的,那么一定是从前向后遍历。
- 举例推导dp数组
以示例1,输入:[7,1,5,3,6,4]为例,dp数组状态如下:
代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
//dp[i][0]表示第i天持有股票的所得现金 dp[i][1]表示第i天不持有股票的所得现金
vector<vector<int>> dp(prices.size(), vector<int>(2));
dp[0][0] = -prices[0];
dp[0][1] = 0;
for (int i = 1; i < prices.size(); i++) {
dp[i][0] = max(dp[i - 1][0], -prices[i]); //第i天刚买入股票 或 之前就买入股票
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]); //第i天卖出股票 或 之前就卖出股票
}
return dp[prices.size() - 1][1];
}
};
优化:从递推公式可以看出,dp[i]只是依赖于dp[i - 1]的状态。因此只需要记录 当前天的dp状态和前一天的dp状态即可,就是使用滚动数组来节省空间。
代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
//采用滚动数组
int len = prices.size();
vector<vector<int>> dp(2, vector<int>(2)); //只记录上次处理以及本次处理
dp[0][0] = -prices[0];
dp[0][1] = 0;
for (int i = 1; i < len; i++){
dp[i % 2][0] = max(dp[(i - 1) % 2][0], -prices[i]); //第i天刚买入股票 或 之前就买入股票
dp[i % 2][1] = max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] + prices[i]); //第i天卖出股票 或 之前就卖出股票
}
return dp[(len - 1) % 2][1];
}
};
思考三:动态规划。处理逻辑:考虑中间盈利相加。
- 确定dp数组(dp table)以及下标的含义
dp[i] 表示下标为i的那天卖出股票所能获取的最大利润
- 确定递推公式
从dp[i]的定义可以看出,dp[i]可以由两个方向推出来
- 如果前面买入股票第 i 天卖出为正盈利,则累加盈利 即:dp[i - 1] + prices[i] - prices[i - 1].
- 如果前面买入股票第 i 天卖出为负盈利,则重新考虑第 i 天买入购票 即:0
同时考虑以上两种情况,取较大值。所以dp[i] = max(0, dp[i - 1] + prices[i] - prices[i - 1]);
当然要记录盈利最大值,每次循环中比较并更新。
- dp数组如何初始化
由递推公式 dp[i] = max(0, dp[i - 1] + prices[i] - prices[i - 1]);可以看出,基础都是要从dp[0]推导出来。如果只有一天,则不能获取任何利润,因此dp[0] = 0.
- 确定遍历顺序
从递推公式可以看出dp[i]都是由dp[i - 1]推导出来的,那么一定是从前向后遍历。
- 举例推导dp数组
以示例1,输入:[7,1,5,3,6,4]为例,dp数组状态如下:
代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<int> dp(prices.size()); //dp[i]表示下标为i的那天卖出股票所能获取的最大利润
dp[0] = 0;
int result = 0;
for (int i = 1; i < prices.size(); i++) { //遍历天数
dp[i] = max(0, dp[i - 1] + prices[i] - prices[i - 1]);
if (result < dp[i]) result = dp[i]; //记录最大值
}
return result;
}
};
Leetcode 122.买卖股票的最佳时机II
题目链接:122 买卖股票的最佳时机II
题干:给你一个整数数组
prices
,其中prices[i]
表示某支股票第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]);
代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
//dp[i][0]表示第i天持有股票的所得现金 dp[i][1]表示第i天不持有股票的所得现金
vector<vector<int>> dp(prices.size(), vector<int>(2));
dp[0][0] = -prices[0];
dp[0][1] = 0;
for (int i = 1; i < prices.size(); i++) {
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); //第i天再次买入股票 或 之前就持有股票
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]); //第i天卖出股票 或 之前就卖出股票
}
return dp[prices.size() - 1][1];
}
};
优化:当然本题也是只需要记录 当前天的dp状态和前一天的dp状态即可,因此仍可以使用滚动数组来节省空间。
代码:
class Solution {
public:
int maxProfit(vector<int>& prices) {
//采用滚动数组
int len = prices.size();
vector<vector<int>> dp(2, vector<int>(2)); //只记录上次处理以及本次处理
dp[0][0] = -prices[0];
dp[0][1] = 0;
for (int i = 1; i < len; i++){
dp[i % 2][0] = max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] -prices[i]); //第i天刚买入股票 或 之前就买入股票
dp[i % 2][1] = max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] + prices[i]); //第i天卖出股票 或 之前就卖出股票
}
return dp[(len - 1) % 2][1];
}
};
自我总结:
从不同角度看待问题,一次买卖还是多次盈利,得出不同动态规划解法。
一次买卖的思路(即不考虑中间盈利)更适配此系列问题。