
总共有5个阶段:
第1阶段,什么没有买卖过,无股票票
第2阶段,买过一次,有股票
第3阶段,卖过一次,无股票
第4阶段,买过二次,有股票
第5节点,卖过两次,无股票
dpij 前i个位置,在第j个节点,获得最大利润是什么
如果当前没有股票,
dpij = dpi-1j
dpij = max(dpij, dpi-1j-1 + pricesi-1 - pricesi-2); 前i-1天,前一个阶段有股票 + 今天卖出去(注意j需要 > 1为,因为j=0的状态没有意义,不存在第0个阶段)
如果当前有股票
dpij = dpi-1j-1 今天买入股票
dpij = max(dpij, dpi-1\[j + pricesi-1 - pricesi-2); 前i-1天有股票,需要累加上当天的股票收益
cpp
class Solution {
public:
/**
* @param prices: Given an integer array
* @return: Maximum profit
*/
int maxProfit(vector<int>& prices) {
// write your code here
int len = prices.size();
if (len <= 0) {
return 0;
}
vector<vector<int>> dp(prices.size() + 1, vector<int>(6));
for (int i = 1; i <= 5; ++i) {
dp[0][i] = INT_MIN;
}
dp[0][1] = 0;
// 第1阶段,什么没有买卖过,无股票票
// 第2阶段,买过一次,有股票
// 第3阶段,卖过一次,无股票
// 第4阶段,买过二次,有股票
// 第5节点,卖过两次,无股票
for (int i = 1; i <= len; ++i) {
for (int j = 1; j <= 5; j += 2) {
dp[i][j] = dp[i - 1][j];
/*j == 1的情况下,没有进行初始化,没有第0个阶段*/
if (i >= 2 && j > 1 && dp[i - 1][j - 1] != INT_MIN) {
dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + prices[i - 1] -
prices[i - 2]);
}
}
for (int j = 2; j <= 5; j += 2) {
dp[i][j] = dp[i - 1][j - 1];
if (i >= 2 && dp[i - 1][j] != INT_MIN) {
dp[i][j] = max(dp[i][j], dp[i - 1][j] + prices[i - 1] -
prices[i - 2]);
}
}
}
int result = 0;
for (int i = 1; i <= 5; i += 2) {
result = max(result, dp[len][i]);
}
return result;
}
};
https://www.jiuzhang.com/problems/info/393
限制k次,需要n=2*k+1
cpp
class Solution {
public:
/**
* @param k: An integer
* @param prices: An integer array
* @return: Maximum profit
*/
int maxProfit(int k, vector<int>& prices) {
// write your code here
int len = prices.size();
if (len <= 0) {
return 0;
}
if (len / 2 < k) {
int result = 0;
for (int i = 1; i < len; ++i) {
if (prices[i] > prices[i - 1]) {
result += prices[i] - prices[i - 1];
}
}
return result;
}
int n = 2 * k + 1;
vector<vector<int>> dp(prices.size() + 1, vector<int>(n + 1));
for (int i = 1; i <= n; ++i) {
dp[0][i] = INT_MIN;
}
dp[0][1] = 0;
// 第1阶段,什么没有买卖过,无股票票
// 第2阶段,买过一次,有股票
// 第3阶段,卖过一次,无股票
// 第4阶段,买过二次,有股票
// 第5节点,卖过两次,无股票
for (int i = 1; i <= len; ++i) {
for (int j = 1; j <= n; j += 2) {
dp[i][j] = dp[i - 1][j];
/*j == 1的情况下,没有进行初始化,没有第0个阶段*/
if (i >= 2 && j > 1 && dp[i - 1][j - 1] != INT_MIN) {
dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + prices[i - 1] -
prices[i - 2]);
}
}
for (int j = 2; j <= n; j += 2) {
dp[i][j] = dp[i - 1][j - 1];
if (i >= 2 && dp[i - 1][j] != INT_MIN) {
dp[i][j] = max(dp[i][j], dp[i - 1][j] + prices[i - 1] -
prices[i - 2]);
}
}
}
int result = 0;
for (int i = 1; i <= n; i += 2) {
result = max(result, dp[len][i]);
}
return result;
}
};
状态机 DP:股票买卖(最多 2 次交易)
这段代码把"最多两次交易"建模成一个5 个状态的自动机 ,dp[i][j] 表示前 i 天处于状态 j 时的最大利润。
一、5 个状态的含义
| 状态 j | 含义 | 奇偶 |
|---|---|---|
| 1 | 未持有股票,还没开始第 1 次交易 | 奇(未持有) |
| 2 | 持有股票,正在进行第 1 次交易 | 偶(持有) |
| 3 | 未持有股票,已完成第 1 次交易 | 奇(未持有) |
| 4 | 持有股票,正在进行第 2 次交易 | 偶(持有) |
| 5 | 未持有股票,已完成第 2 次交易 | 奇(未持有) |
状态转移是一条链:
text
1 → 2 → 3 → 4 → 5
未持有 持有 未持有 持有 未持有
-
偶数状态(2、4):手里有股票
-
奇数状态(1、3、5):手里没股票
-
每次"买入"是
奇数 → 偶数,每次"卖出"是偶数 → 奇数
二、转移方程
未持有状态(j = 1, 3, 5)
java
dp[i][j] = dp[i - 1][j]; // 继续不持有
if (j > 1 && i > 1 && dp[i-1][j-1] != MIN_VALUE) {
dp[i][j] = max(dp[i][j], dp[i-1][j-1] + prices[i-1] - prices[i-2]); // 今天卖出
}
-
继续不持有:昨天也不持有,今天什么都不做。
-
今天卖出 :昨天持有(状态
j-1),今天卖掉,赚取今天的差价prices[i-1] - prices[i-2]。
持有状态(j = 2, 4)
java
dp[i][j] = dp[i - 1][j - 1]; // 今天买入
if (i > 1 && dp[i-1][j] != MIN_VALUE) {
dp[i][j] = max(dp[i][j], dp[i-1][j] + prices[i-1] - prices[i-2]); // 继续持有
}
-
今天买入 :昨天未持有(状态
j-1),今天买入,利润不变。 -
继续持有 :昨天持有,今天继续持有,累积今天的差价。
三、关键技巧:为什么用"差价"而不是"价格"
通常的写法是:
java
// 买入:减去买入价格
dp[i][2] = dp[i-1][1] - prices[i-1];
// 卖出:加上卖出价格
dp[i][3] = dp[i-1][2] + prices[i-1];
但这个解法用的是每日差价累积:
text
持有期间:dp[i][j] += prices[i-1] - prices[i-2] // 每天累积涨跌
卖出当天:dp[i][j] += prices[i-1] - prices[i-2] // 再加一次最后一天的差价
为什么等价?举个例子
假设第 2 天买入(价格 10),第 5 天卖出(价格 18):
差价写法:
| 天数 | 动作 | 累积利润 |
|---|---|---|
| 第 2 天 | 买入,进入状态 2 | dp[2][2] = dp[1][1] = 0 |
| 第 3 天 | 继续持有 | dp[3][2] = 0 + (p3 - p2) |
| 第 4 天 | 继续持有 | dp[4][2] = (p3 - p2) + (p4 - p3) = p4 - p2 |
| 第 5 天 | 卖出,进入状态 3 | dp[5][3] = dp[4][2] + (p5 - p4) = p5 - p2 |
最终 dp[5][3] = p5 - p2 = 18 - 10 = 8 ✅
标准写法 :dp[5][3] = dp[4][2] + p5 = (dp[1][1] - p2) + p5 = p5 - p2 = 8 ✅
两者结果完全相同。差价写法的好处是不需要存储负的买入价格,所有状态都是"已实现的利润",更直观。
四、初始化
java
for (int k = 1; k <= 5; k++) {
dp[0][k] = Integer.MIN_VALUE; // 第 0 天,除了状态 1,其他都不可达
}
dp[0][1] = 0; // 第 0 天未持有,利润 0
-
dp[0][1] = 0:还没开始交易,利润为 0。 -
其他状态设为
MIN_VALUE:表示"第 0 天不可能持有股票或完成交易"。
转移时用 dp[i-1][j-1] != MIN_VALUE 检查,防止从不可达状态转移。
五、最终答案
java
int res = 0;
for (int j = 1; j <= 5; j += 2) { // 状态 1, 3, 5(未持有)
res = Math.max(res, dp[n][j]);
}
最后一天必须未持有股票(手里有股票不算完成交易),所以在状态 1、3、5 中取最大值。
-
状态 1:一次都没交易,利润 0
-
状态 3:完成 1 次交易
-
状态 5:完成 2 次交易
取最大就是"最多 2 次交易"的最优解。
六、举例走一遍
text
prices = [3, 3, 5, 0, 0, 3, 1, 4]
最优策略:第 4 天(价格 0)买入,第 6 天(价格 3)卖出,赚 3;第 7 天(价格 1)买入,第 8 天(价格 4)卖出,赚 3。总利润 6。
DP 表最终 dp[8][5] = 6,dp[8][3] 也可能等于 3(只做第一次交易),dp[8][1] = 0。取最大值 6 ✅
七、这个解法的优缺点
| 优点 | 缺点 |
|---|---|
| 状态机清晰,5 个状态一目了然 | 状态数随交易次数线性增长,k 次交易需要 2k+1 个状态 |
| 差价写法不需要存负值,所有状态都是"已实现利润" | 差价写法对初学者不太直观 |
| 可以直接推广到"最多 k 次交易"(LeetCode 188) | 需要处理 MIN_VALUE 防止溢出 |
八、推广到 k 次交易
把 5 换成 2k+1,状态含义不变:
text
1: 未持有,0 次交易
2: 持有,第 1 次进行中
3: 未持有,1 次完成
4: 持有,第 2 次进行中
...
2k+1: 未持有,k 次完成
转移逻辑完全一样,只是循环范围变大。这就是 LeetCode 188「买卖股票的最佳时机 IV」的标准解法。
九、一句话总结
dp[i][j] 表示前 i 天处于状态 j 的最大利润,状态机是"未持有 → 持有 → 未持有 → 持有 → 未持有",每次买入/卖出在奇偶状态间切换,用每日差价累积利润,最后在未持有状态中取最大值。