LeetCode 123:买卖股票的最佳时机 III
1. 题目核心
给定每天的股票价格 prices[i],最多完成 2 笔交易,求最大利润。一笔完整交易是:
text
买入 → 卖出
并且必须卖掉当前股票以后才能再次买入。注意是最多 2 笔 ,不是必须做 2 笔,因此可以做 0、1、2 笔交易。
例如:
text
prices = [3,3,5,0,0,3,1,4]
最佳方案:
text
0 买 → 3 卖:赚 3
1 买 → 4 卖:赚 3
总利润 = 6
2. 思路一:枚举两笔交易
最直接的想法是枚举:
text
第一次:哪天买 → 哪天卖
第二次:哪天买 → 哪天卖
然后保证:
text
第一次卖出时间 ≤ 第二次买入时间
计算所有合法方案的利润并取最大值。思路正确,但组合很多,会产生大量重复计算,因此不适合作为最优解。
3. 思路二:前后缀最大利润
因为最多只有两笔交易,也可以枚举"两笔交易的分界点"。例如在第 i 天切开:
text
[ 左半部分 ] | [ 右半部分 ]
最多1笔 最多1笔
分别计算:
text
left[i] = 第0~i天最多做1笔交易的最大利润
right[i] = 第i~最后一天最多做1笔交易的最大利润
然后:
text
答案 = max(left[i] + right[i])
这种方法时间复杂度 O(n),但需要额外的前后缀数组,空间复杂度 O(n)。
4. 最优思路:状态机动态规划
因为最多交易两次,所以真正需要记录的状态只有:
text
buy[1] = 第一次买入以后,持有股票时最好的收益状态
sell[1] = 第一次卖出以后,不持股时的最大利润
buy[2] = 第二次买入以后,持有股票时最好的收益状态
sell[2] = 第二次卖出以后,不持股时的最大利润
整个过程就是:
text
sell[0]
↓ 第1次买
buy[1]
↓ 第1次卖
sell[1]
↓ 第2次买
buy[2]
↓ 第2次卖
sell[2]
其中:
text
sell[0] = 0
表示一笔交易都没做时,利润为 0。
5. 最核心的两个公式
5.1 买入公式
cpp
buy[j] = max(buy[j], sell[j - 1] - price);
这里是在比较两种方案:
text
方案1:今天不买
→ 继续保持以前的 buy[j]
方案2:今天进行第 j 次买入
→ 前 j-1 笔交易赚的钱 - 今天买股票花的钱
因此:
text
buy[j]
=
max(
以前第j次买入后的最好状态,
今天第j次买入后的状态
)
例如第一笔交易已经赚了 4:
text
sell[1] = 4
今天股票价格为 1,如果今天进行第二次买入:
text
sell[1] - price
= 4 - 1
= 3
所以:
text
buy[2] = 3
这个 3 不是第二笔交易赚了3元,而是:
text
第一笔已经赚4元
- 第二次买股票花1元
= 当前整个交易过程的净收益状态3元
所以买入可以记成一句话:
前一笔卖完赚的钱 - 当前买入价格。
5.2 卖出公式
cpp
sell[j] = max(sell[j], buy[j] + price);
同样比较两种方案:
text
方案1:今天不卖
→ 保持以前的 sell[j]
方案2:今天进行第 j 次卖出
→ 当前 buy[j] 状态 + 今天卖股票得到的钱
例如:
text
buy[2] = 3
今天价格 = 4
今天第二次卖出:
text
3 + 4 = 7
于是:
text
sell[2] = 7
卖出可以记成:
买入以后剩下的收益状态 + 当前卖出价格。
6. 两个公式怎么连起来
最重要的一张图:
text
sell[j-1]
↓
减去当前股价
↓
buy[j]
↓
加上当前股价
↓
sell[j]
因此:
cpp
buy[j] = max(buy[j], sell[j - 1] - price);
sell[j] = max(sell[j], buy[j] + price);
所有 max() 的含义完全一样:
以前保存的最好方案 vs 今天进行操作得到的新方案,留下更好的那个。
7. 为什么买入也要取 max
例如:
text
prices = [3,2]
第一天价格 3:
text
buy[1] = -3
第二天价格变成 2:
text
以前买:-3
今天买:-2
显然:
text
-2 > -3
所以:
cpp
buy[1] = max(-3, -2) = -2;
它表示:
text
原来3元买入不划算
现在改成2元买入更好
因此 buy 虽然经常是负数,仍然是越大越好。
8. 为什么 DP 不需要提前决定哪天买卖
DP 不会真的在价格 6 时宣布"我已经卖了,以后不能反悔"。它只是在保存:
text
截至今天,sell[1] 状态下能够达到的最好成绩是多少?
例如:
text
prices = [2,6,5,100]
价格 6 时:
text
sell[1] = 4
到了 5:
text
以前最好 = 4
今天卖 = 5 - 2 = 3
sell[1] = max(4,3) = 4
最后价格 100:
text
以前最好 = 4
今天卖 = 100 - 2 = 98
sell[1] = max(4,98) = 98
所以即使最后一天才是最高价,DP 也会自动把答案更新成最后一天卖。
9. 为什么不需要一定交易两次
题目说的是:
text
最多交易2次
例如:
text
prices = [7,6,4,3,1]
一直下跌,最优策略就是:
text
什么都不做
利润 = 0
因此:
text
sell[1] = 0
sell[2] = 0
最终答案也是 0。如果只做一笔交易最优,也不需要强行进行第二笔。
10. 示例完整推导
对于:
text
prices = [3,3,5,0,0,3,1,4]
状态变化如下:
| 当前价格 | buy1 | sell1 | buy2 | sell2 |
|---|---|---|---|---|
| 3 | -3 | 0 | -3 | 0 |
| 3 | -3 | 0 | -3 | 0 |
| 5 | -3 | 2 | -3 | 2 |
| 0 | 0 | 2 | 2 | 2 |
| 0 | 0 | 2 | 2 | 2 |
| 3 | 0 | 3 | 2 | 5 |
| 1 | 0 | 3 | 2 | 5 |
| 4 | 0 | 4 | 2 | 6 |
最终:
text
sell[2] = 6
对应一种最佳交易方案:
text
0 → 3:赚3
1 → 4:赚3
总利润 = 6
11. C++ 动态规划代码
cpp
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<int> buy(3, -1000000000);
vector<int> sell(3, 0);
for (int i = 0; i < prices.size(); ++i) {
for (int j = 2; j >= 1; --j) {
buy[j] = max(buy[j], sell[j - 1] - prices[i]);
sell[j] = max(sell[j], buy[j] + prices[i]);
}
}
return sell[2];
}
};
C++代码逐句对应
cpp
vector<int> buy(3, -1000000000);
创建:
text
buy[0], buy[1], buy[2]
实际使用 buy[1]、buy[2],初始设成非常小的数,因为一开始还不存在"已经买入"的状态。
cpp
vector<int> sell(3, 0);
表示不交易时利润为 0,其中最重要的是:
text
sell[0] = 0
for (int j = 2; j >= 1; --j)
依次处理:
text
第2笔交易
第1笔交易
倒序可以保证:
cpp
buy[j]
使用的:
cpp
sell[j - 1]
还是之前保存的状态,而不是这一轮刚更新出来的值。
12. Java 动态规划代码
Java 使用的也是完全相同的动态规划算法。
java
class Solution {
public int maxProfit(int[] prices) {
int[] buy = {-1000000000, -1000000000, -1000000000};
int[] sell = new int[3];
for (int price : prices) {
for (int j = 2; j >= 1; j--) {
buy[j] = Math.max(buy[j], sell[j - 1] - price);
sell[j] = Math.max(sell[j], buy[j] + price);
}
}
return sell[2];
}
}
13. Java 语法解释
13.1 一维数组
java
int[] buy = {-1000000000, -1000000000, -1000000000};
表示创建整数数组:
text
buy[0]
buy[1]
buy[2]
对应 C++:
cpp
vector<int> buy(3, -1000000000);
13.2 new int[3]
java
int[] sell = new int[3];
创建长度为 3 的整数数组。Java 的 int 数组默认全部初始化为 0,因此得到:
text
[0,0,0]
13.3 增强 for 循环
java
for (int price : prices)
表示依次取出 prices 中的每一个元素,相当于 C++:
cpp
for (int price : prices)
13.4 Math.max
Java:
java
Math.max(a, b)
对应 C++:
cpp
max(a, b)
例如:
java
buy[j] = Math.max(buy[j], sell[j - 1] - price);
就是比较:
text
以前买入的状态
vs
今天买入的状态
14. Python 动态规划代码
Python 同样使用完全相同的动态规划算法。
python
class Solution:
def maxProfit(self, prices):
buy = [-10**9] * 3
sell = [0] * 3
for price in prices:
for j in range(2, 0, -1):
buy[j] = max(buy[j], sell[j - 1] - price)
sell[j] = max(sell[j], buy[j] + price)
return sell[2]
15. Python 语法解释
15.1 创建数组
python
buy = [-10**9] * 3
10**9 表示:
text
10的9次方 = 1000000000
因此:
python
[-10**9] * 3
得到:
text
[-1000000000, -1000000000, -1000000000]
sell = [0] * 3
得到:
text
[0,0,0]
15.2 遍历数组
python
for price in prices:
表示依次拿出每天的股票价格。
15.3 倒序循环
python
for j in range(2, 0, -1):
range(开始, 结束, 步长),因此这里产生:
text
2, 1
注意不会包含结束值 0。
15.4 max
python
max(a, b)
返回两个值中较大的一个,因此:
python
sell[j] = max(sell[j], buy[j] + price)
表示:
text
以前卖出的最大利润
vs
今天卖出的利润
16. 复杂度
最多只有两笔交易,每一天只更新 buy[1]、sell[1]、buy[2]、sell[2]:
text
时间复杂度:O(n)
空间复杂度:O(1)
虽然代码使用了两个长度为 3 的数组,但数组大小固定,不随 n 增长,所以空间复杂度仍然是 O(1)。
17. 与 LeetCode 188 的关系
LeetCode 123:
text
最多交易2次
所以:
cpp
for (int j = 2; j >= 1; --j)
LeetCode 188:
text
最多交易k次
直接改成:
cpp
for (int j = k; j >= 1; --j)
两个核心公式完全不变:
cpp
buy[j] = max(buy[j], sell[j - 1] - price);
sell[j] = max(sell[j], buy[j] + price);
18. 核心记忆
先记状态:
text
buy[j] = 第j次买入以后,持股状态下最好的总收益
sell[j] = 第j次卖出以后,不持股状态下的最大总利润
再记资金流:
text
sell[j-1]
↓ 买入要花钱,所以减价格
buy[j]
↓ 卖出收钱,所以加价格
sell[j]
对应两条公式:
cpp
buy[j] = max(buy[j], sell[j - 1] - price);
sell[j] = max(sell[j], buy[j] + price);
最后只记一句:
买 = 前一次卖完的钱 - 当前价格;卖 = 当前买入状态 + 当前价格;每次都和以前的最好结果取
max。