LeetCode 188:买卖股票的最佳时机 IV
1. 题目核心
给定股票每天的价格 prices[i] 和整数 k,最多进行 k 笔完整交易,每笔交易必须是:
text
买入 → 卖出
并且必须卖掉手里的股票之后才能再次买入。注意题目说的是最多 k 次 ,不是必须交易 k 次,所以可以交易 0、1、2...k 次。
2. 思路一:枚举所有买卖方案
最直观的方法是枚举:
text
第一次哪天买 → 哪天卖
第二次哪天买 → 哪天卖
......
例如:
text
prices = [3,2,6,5,0,3]
k = 2
可以枚举出:
text
2 买 → 6 卖,利润 4
0 买 → 3 卖,利润 3
总利润 = 7
这种方法的问题是组合数量非常多。第一笔交易确定以后还要继续枚举第二笔、第三笔,因此会出现大量重复计算,不适合直接实现。
3. 思路二:最直观的动态规划
3.1 DP 状态
定义:
text
dp[t][i]
表示:
在第
0~i天中,最多完成t笔交易时能够获得的最大利润。
例如:
text
dp[2][5]
表示:
前 6 天里最多完成 2 笔交易能够获得的最大利润。
3.2 到第 i 天时只有两种情况
对于:
text
dp[t][i]
第一种情况:今天不卖股票。那么最大利润就是昨天已经得到的:
text
dp[t][i - 1]
第二种情况:今天卖股票 。假设第 j 天买入,第 i 天卖出,这一笔交易的利润为:
text
prices[i] - prices[j]
但是在第 j 天买入之前,还可以已经完成最多 t-1 笔交易,所以总利润为:
text
前面 t-1 笔交易的最大利润
+
当前这一笔交易的利润
即:
text
dp[t-1][j-1] + prices[i] - prices[j]
所以完整公式为:
text
dp[t][i]
=
max(
dp[t][i-1],
dp[t-1][j-1] + prices[i] - prices[j]
)
由于买入日 j 不确定,因此实际上还需要把前面所有可能的 j 都尝试一遍:
text
dp[t][i]
=
max(
dp[t][i-1],
max(
dp[t-1][j-1] + prices[i] - prices[j]
)
)
3.3 max 到底在比较什么
它并不抽象,本质就是:
text
以前已经找到的最好方案
vs
今天卖掉产生的新方案
谁利润高,就保留谁。
例如:
text
prices = [3,2,6,5]
到价格 6 时:
text
2 买 → 6 卖 = 4
所以最大利润变成 4。
第二天价格为 5:
text
以前最好利润 = 4
今天卖 = 5 - 2 = 3
所以:
text
max(4,3) = 4
说明今天不卖更好。
3.4 这个 DP 的问题
i 要遍历所有天,t 要遍历所有交易次数,还需要枚举买入日 j:
text
时间复杂度:O(k × n²)
可以继续优化。
4. 最优思路:buy / sell 状态动态规划
把每一笔交易拆成两个状态:
text
第1次买 → 第1次卖 → 第2次买 → 第2次卖 → ...
对于第 t 笔交易定义:
text
buy[t] = 第 t 次买入以后,目前能够保留的最好收益状态
sell[t] = 第 t 次卖出以后,目前能够获得的最大利润
例如 k = 2:
text
buy[1] → sell[1] → buy[2] → sell[2]
5. 两个核心公式
5.1 买入公式
text
buy[t] = max(
buy[t],
sell[t-1] - price
)
含义:
text
方案1:今天不买
→ 保留以前的 buy[t]
方案2:今天买
→ 前 t-1 笔交易已经赚的钱 - 今天买股票花的钱
所以:
text
buy[t] = max(以前买的最好状态, 今天买的状态)
例如第二次买:
text
sell[1] = 4
price = 0
表示第一笔已经赚了 4,现在价格为 0:
text
buy[2] = 4 - 0 = 4
也就是:
text
第一笔交易赚4
→ 0元买入第二次股票
→ 当前整体收益状态仍然为4
5.2 卖出公式
text
sell[t] = max(
sell[t],
buy[t] + price
)
含义:
text
方案1:今天不卖
→ 保留以前的 sell[t]
方案2:今天卖
→ 第 t 次买入后的状态 + 今天卖股票得到的钱
所以:
text
sell[t] = max(以前卖出的最大利润, 今天卖出的利润)
6. 两条公式如何衔接
整个过程可以直接画成:
text
sell[t-1]
↓
前 t-1 笔交易已经获得的利润
↓
减去今天股价
↓
buy[t]
↓
以后加上某一天的股价卖出
↓
sell[t]
所以核心只有:
text
buy[t] = max(buy[t], sell[t-1] - price)
sell[t] = max(sell[t], buy[t] + price)
其中所有 max 都是在比较:
text
以前最好的结果
vs
今天进行操作得到的新结果
7. 示例完整推导
text
prices = [3,2,6,5,0,3]
k = 2
状态:
text
buy1 sell1 buy2 sell2
每天更新后的结果:
| 当前价格 | buy1 | sell1 | buy2 | sell2 |
|---|---|---|---|---|
| 3 | -3 | 0 | -3 | 0 |
| 2 | -2 | 0 | -2 | 0 |
| 6 | -2 | 4 | -2 | 4 |
| 5 | -2 | 4 | -1 | 4 |
| 0 | 0 | 4 | 4 | 4 |
| 3 | 0 | 4 | 4 | 7 |
关键位置是价格 6:
text
buy1 = -2
sell1 = -2 + 6 = 4
表示:
text
2 买 → 6 卖
第一笔赚 4
后来价格变成 0:
text
buy2 = sell1 - 0
= 4
表示:
text
第一笔已经赚 4
然后 0 元进行第二次买入
最后价格为 3:
text
sell2 = buy2 + 3
= 4 + 3
= 7
最终:
text
2 → 6,赚4
0 → 3,赚3
总利润 = 7
8. 为什么不用一定交易 k 次
题目要求:
text
最多 k 次
而不是:
text
必须 k 次
例如:
text
prices = [10,8,6,4,2]
无论 k 多大,最优方案都是:
text
一次都不交易
利润 = 0
如果最后一天才是最高价,也完全没问题。例如:
text
prices = [3,2,4,5,100]
buy[1] 会一直保留最好的买入状态:
text
2 元买入
到最后一天:
text
sell[1] = -2 + 100 = 98
因此 DP 并不会提前强制卖出。
9. C++ 最优解
cpp
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
vector<int> buy(k + 1, -1000000000);
vector<int> sell(k + 1, 0);
for (int price : prices) {
for (int t = k; t >= 1; --t) {
sell[t] = max(sell[t], buy[t] + price);
buy[t] = max(buy[t], sell[t - 1] - price);
}
}
return sell[k];
}
};
代码对应关系
cpp
sell[t] = max(sell[t], buy[t] + price);
表示:
text
今天不卖
vs
今天进行第 t 次卖出
buy[t] = max(buy[t], sell[t - 1] - price);
表示:
text
今天不买
vs
前 t-1 笔交易完成以后,今天进行第 t 次买入
交易次数从后往前:
cpp
for (int t = k; t >= 1; --t)
主要是为了让 buy[t] 使用的 sell[t-1] 仍然是当前这一轮更新之前保存的状态。
10. Java 最优解
java
class Solution {
public int maxProfit(int k, int[] prices) {
int[] buy = new int[k + 1];
int[] sell = new int[k + 1];
for (int t = 1; t <= k; t++) {
buy[t] = -1000000000;
}
for (int price : prices) {
for (int t = k; t >= 1; t--) {
sell[t] = Math.max(sell[t], buy[t] + price);
buy[t] = Math.max(buy[t], sell[t - 1] - price);
}
}
return sell[k];
}
}
11. Java 语法解释
11.1 创建一维数组
java
int[] buy = new int[k + 1];
表示创建一个长度为 k+1 的整数数组,对应 C++:
cpp
vector<int> buy(k + 1);
Java 的 int 数组默认所有元素都是:
text
0
11.2 增强 for 循环
java
for (int price : prices)
表示依次取出 prices 中的每个元素,相当于 C++:
cpp
for (int price : prices)
也就是:
text
第一天价格
第二天价格
第三天价格
...
11.3 Math.max
Java:
java
Math.max(a, b)
对应 C++:
cpp
max(a, b)
所以:
java
sell[t] = Math.max(sell[t], buy[t] + price);
就是:
text
以前的最大利润
和
今天卖出的利润
取更大的一个
11.4 t--
java
for (int t = k; t >= 1; t--)
表示:
text
k, k-1, k-2, ..., 1
和 C++ 的写法基本完全一样。
12. Python 最优解
python
class Solution:
def maxProfit(self, k, prices):
buy = [-10**9] * (k + 1)
sell = [0] * (k + 1)
for price in prices:
for t in range(k, 0, -1):
sell[t] = max(sell[t], buy[t] + price)
buy[t] = max(buy[t], sell[t - 1] - price)
return sell[k]
13. Python 语法解释
13.1 创建数组
python
sell = [0] * (k + 1)
如果:
text
k = 3
得到:
python
[0, 0, 0, 0]
同理:
python
buy = [-10**9] * (k + 1)
表示创建 k+1 个非常小的初始值。
13.2 10**9
Python:
python
10**9
表示:
text
10 的 9 次方
= 1000000000
所以:
python
-10**9
就是:
text
-1000000000
13.3 遍历数组
python
for price in prices:
表示每次取出一个股票价格,相当于 C++:
cpp
for (int price : prices)
13.4 倒序遍历
python
for t in range(k, 0, -1):
假设:
text
k = 4
那么 t 依次为:
text
4, 3, 2, 1
其中:
text
range(开始值, 结束值, 步长)
所以:
python
range(k, 0, -1)
表示从 k 开始,每次减 1,直到 1。
13.5 Python 的 max
python
max(a, b)
直接返回两者中较大的一个,所以:
python
buy[t] = max(buy[t], sell[t - 1] - price)
就是:
text
以前的第 t 次买入状态
vs
今天进行第 t 次买入后的状态
14. 复杂度
最优动态规划中:
text
每天遍历一次:n
每一天遍历 k 个交易状态:k
所以:
text
时间复杂度:O(n × k)
空间复杂度:O(k)
相比直观 DP 的:
text
O(k × n²)
省掉了对所有历史买入日的重复枚举。
15. 核心记忆
状态
text
buy[t] = 第 t 次买入后的最好状态
sell[t] = 第 t 次卖出后的最大利润
买入
text
buy[t] = max(
以前的 buy[t],
sell[t-1] - 今天价格
)
卖出
text
sell[t] = max(
以前的 sell[t],
buy[t] + 今天价格
)
一句话理解
text
sell[t-1]
↓ 减去股价
buy[t]
↓ 加上股价
sell[t]
每个 max() 永远只是在比较:
text
以前的最好方案
vs
今天操作产生的新方案
题目是最多交易 k 次,不是必须交易 k 次;DP 也不会提前固定哪天买卖,而是在每一天持续保留当前能达到的最佳状态。