提示:DDU,供自己复习使用。欢迎大家前来讨论~
买卖股票的最佳时机相关题目
题目一:121. 买卖股票的最佳时机
[[121. 买卖股票的最佳时机](https://leetcode.cn/problems/combinations/)
解题思路:
- 创建一个二维数组
dp
来存储每一天的两种状态(持有股票和不持有股票)。 - 从第0天开始,根据股票价格更新
dp
数组。 - 每一天的状态转移只依赖于前一天的状态,因此可以顺序遍历。
- 最终答案即为
dp[n-1][1]
,表示在整个时间段内不持有股票时的最大利润。
状态解释:
- 持有股票(
dp[i][0]
):有两种情况,一是之前从未购买股票,在第i
天购买;二是之前已经购买并持有到第i
天。因此,dp[i][0]
可以是从前一天持有状态转移来的(dp[i-1][0]
),或者是在第i
天新购买股票(-prices[i]
,表示花费)。 - 不持有股票(
dp[i][1]
):也有两种情况,一是之前从未购买过股票;二是在第i
天或之前某天卖出股票。因此,dp[i][1]
可以是从前一天不持有状态转移来的(dp[i-1][1]
),或者是在第i
天卖出股票(dp[i-1][0] + prices[i]
,表示之前持有的利润加上今天卖出的利润)。
动态规划五部曲分析:
-
确定 dp 数组及其含义:
dp[i][0]
表示第i
天手里持有股票的最大利润。dp[i][1]
表示第i
天手里不持有股票的最大利润。
-
初始化:
dp[0][0]
初始化为-prices[0]
,表示第0天买了股票后的利润(负数)。dp[0][1]
初始化为0
,表示第0天手里不持有股票的利润。
-
递推公式:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i])
:第i
天持有股票的状态取决于前一天是否持有股票(dp[i-1][0]
),或者前一天卖掉股票后买入(dp[i-1][1] - prices[i]
)。dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i])
:第i
天不持有股票的状态取决于前一天是否持有股票并今天卖掉(dp[i-1][0] + prices[i]
),或者前一天就不持有股票(dp[i-1][1]
)。
-
遍历顺序:
- 遍历天数,对于每一天,根据股票价格更新
dp[i][0]
和dp[i][1]
。
- 遍历天数,对于每一天,根据股票价格更新
-
最终结果:
- 遍历完成后,
dp[n][1]
表示在整个时间段内不持有股票时的最大利润,这是我们要求的答案。
- 遍历完成后,
个人理解:
- 递推公式中的
dp[i][0]
维护了到第i
天为止,持有股票时的最大利润。 - 递推公式中的
dp[i][1]
维护了到第i
天为止,不持有股票时的最大利润。
最终思路:
- 遍历每一天,对于每一天,考虑持有或不持有股票,并更新
dp[i][0]
和dp[i][1]
。 - 每一天的状态只依赖于前一天的状态,因此可以顺序遍历。
- 最终答案即为
dp[n][1]
,表示在整个时间段内不持有股票时的最大利润。
以上分析完毕,C++代码如下:
// 版本一
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
if (len == 0) return 0;
vector<vector<int>> dp(len, vector<int>(2));
dp[0][0] -= prices[0];
dp[0][1] = 0;
for (int i = 1; i < len; i++) {
dp[i][0] = max(dp[i - 1][0], -prices[i]);
dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
}
return dp[len - 1][1];
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(n)
从递推公式可以看出,dp[i]只是依赖于dp[i - 1]的状态。
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状态和前一天的dp状态就可以了,可以使用滚动数组来节省空间,代码如下:(不熟练原理,可以不需要掌握这个)
// 版本二
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
vector<vector<int>> dp(2, vector<int>(2)); // 注意这里只开辟了一个2 * 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]);
dp[i % 2][1] = max(dp[(i - 1) % 2][1], prices[i] + dp[(i - 1) % 2][0]);
}
return dp[(len - 1) % 2][1];
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1)
题目二:122.买卖股票的最佳时机II
解题思路:
本题和上题的唯一区别是本题股票可以买卖多次了(注意只有一只股票,所以再次购买前要出售掉之前的股票)
在动规五部曲中,这个区别主要是体现在递推公式上,其他都和121. 买卖股票的最佳时机 (opens new window)一样的。
重点讲一讲递推公式。
这里重申一下dp数组的含义:
- dp[i][0] 表示第i天持有股票所得现金。
- dp[i][1] 表示第i天不持有股票所得最多现金
如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来
- 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
- 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i]
注意这里和121. 买卖股票的最佳时机唯一不同的地方,就是推导dp[i][0]的时候,第i天买入股票的情况。
在121. 买卖股票的最佳时机中,因为股票全程只能买卖一次,所以如果买入股票,那么第i天持有股票即dp[i][0]一定就是 -prices[i]。
而本题,因为一只股票可以买卖多次,所以当第i天买入股票的时候,所持有的现金可能有之前买卖过的利润。
那么第i天持有股票即dp[i][0],如果是第i天买入股票,所得现金就是昨天不持有股票的所得现金 减去 今天的股票价格 即:dp[i - 1][1] - prices[i]。
再来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来
- 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
- 第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金即:prices[i] + dp[i - 1][0]
代码如下:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int len = prices.size();
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]); // 注意这里是和121. 买卖股票的最佳时机唯一不同的地方。
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
return dp[len - 1][1];
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(n)
唯一的区别在:
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
- 多次交易的情况:与只能买卖一次股票的问题不同,当允许多次交易时,我们需要考虑之前所有可能的买卖操作对当前决策的影响。
- 买入股票时的考虑:在决定买入股票时,不仅要考虑之前持有股票的情况(
dp[i-1][0]
),还要考虑之前卖出股票后累积的利润(dp[i-1][1]
)。因此,买入股票的操作可以表示为dp[i-1][1] - prices[i]
,这反映了在第i
天买入股票后持有的状态。 - 递推公式的调整:在多次交易的情况下,动态规划的递推公式需要反映这种累积的利润。这意味着在计算
dp[i][0]
(第i
天持有股票的最大利润)时,应该考虑从卖出状态转入持有状态的可能性。 - 深刻理解题目:理解了在允许多次交易的情况下,买入股票时不仅要考虑直接的购买成本,还要考虑之前通过交易获得的利润,是深刻理解这类股票交易问题的关键。
题目三: 123.买卖股票的最佳时机III
解题思路
- 状态定义:
dp[i][j]
表示在第i
天,处于状态j
时的最大现金。状态j
可以是五种之一:无操作、第一次持有股票、第一次卖出股票、第二次持有股票、第二次卖出股票。
- 递推公式:
dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1])
:表示在第i
天买入股票,可以是今天买入,也可以是延续前一天的买入状态。dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])
:表示在第i
天卖出股票,可以是今天卖出,也可以是延续前一天的卖出状态。dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i])
:第二次买入股票。dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i])
:第二次卖出股票。
- 初始化:
dp[0][0] = 0
:第0天无操作。dp[0][1] = -prices[0]
:第0天买入股票。dp[0][2] = 0
:第0天卖出股票(当天买入当天卖出)。dp[0][3] = -prices[0]
:第0天第二次买入股票(假设先前已经卖出)。dp[0][4] = 0
:第0天第二次卖出股票。
- 遍历顺序:
- 由于每一天的计算依赖于前一天的结果,因此需要从前向后遍历。
- 举例推导:
- 以
[1,2,3,4,5]
为例,逐步填充dp
数组,考虑每一天可能的买卖操作,并更新状态。
- 以
可以看到红色框为最后两次卖出的状态。
现在最大的时候一定是卖出的状态,而两次卖出的状态现金最大一定是最后一次卖出。如果想不明白的录友也可以这么理解:如果第一次卖出已经是最大值了,那么我们可以在当天立刻买入再立刻卖出。所以dp[4][4]已经包含了dp[4][2]的情况。也就是说第二次卖出手里所剩的钱一定是最多的。
所以最终最大利润是dp[4][4]
以上五部都分析完了,不难写出如下代码:
// 版本一
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.size() == 0) return 0;
vector<vector<int>> dp(prices.size(), vector<int>(5, 0));
dp[0][1] = -prices[0];
dp[0][3] = -prices[0];
for (int i = 1; i < prices.size(); i++) {
dp[i][0] = dp[i - 1][0];
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]);
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
}
return dp[prices.size() - 1][4];
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(n × 5)
总结
买卖股票的最佳时机问题是一个经典的动态规划问题,旨在确定在给定的股票价格列表中买卖股票的最佳时机以最大化利润。
- 状态定义:
- 定义
dp[i][0]
表示第i
天不持有股票的最大利润。 - 定义
dp[i][1]
表示第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])
:持有股票的状态可以是前一天就持有的延续,或者是在今天买入了股票。
- 初始化和遍历:
- 初始化
dp[0][0]
为 0,表示在第0天不持有股票的利润为0。 - 初始化
dp[0][1]
为-prices[0]
,表示在第0天如果买入股票,则利润为负数(因为你花了钱买股票)。 - 从第1天开始遍历到最后一天,根据每天的股票价格更新
dp[i][0]
和dp[i][1]
。
- 初始化
买卖股票的最佳时机问题通过动态规划方法,考虑每天是否持有股票的两种状态,并通过状态转移方程来更新每天的最大利润。最终,不持有股票的最大利润 dp[n][0]
就是整个时间段内可以获得的最大利润。