买卖股票系列:从状态机彻底吃透动态规划

哈喽,大家好呀,我是蔚蓝🥰,今天我们要进行一场彻底的「买卖股票」大通关!

之前有很多小伙伴跟我说,股票题一道道刷起来太痛苦了。其实,如果你能看透这背后的「状态机」本质,无论题目怎么变——是限制次数、带冷冻期,还是收手续费,甚至允许「做空」,它们在你的眼里都只是几个状态转来转去的小游戏。

今天这一篇,我把笔记里所有的核心案例和代码都翻了出来,咱们一次性把这块硬骨头啃碎。

1. 股票题的核心:状态转移公式

在进入具体代码之前,请大家在大脑里植入一个万能的思维模型:第 i 天结束后,我处于什么状态?

不管是哪一道股票题,核心公式永远只有这两行(或者是它们的变体):

  • 今天空仓 (dp[i][0]) = max(昨天就不持有, 昨天持有 + 今天按市场价卖了)
  • 今天持仓 (dp[i][1]) = max(昨天就持有, 「之前的钱」 - 今天买入花的钱)

这里的 「之前的钱」 就是解开所有难题的钥匙。


2. 逐题分析:从简单到「变态」

关卡 1: Stock I (一辈子只能谈一次恋爱)

因为你只能买一次,所以你买入时的起点永远是 0。

1
2
3
// 核心逻辑
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], -prices[i]); // 之前没钱,买入变负债

关卡 2: Stock II (无限次交易)

你可以带着之前赚到的血汗钱继续投。

1
2
// 核心逻辑
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]); // 带着昨天的利润进场

关卡 3: Stock III & IV (有额度限制的交易)

引入了交易次数 k。不仅要看持不持有,还要看这是你的第几次机会。

1
2
3
4
5
6
7
8
9
// Stock IV 三维数组版:dp[天数][次数][状态]
for (int i = 1; i < n; i++) {
for (int j = 1; j <= k; j++) {
// 第 j 次持有:昨天就拿住了,或者今天消耗一次机会进场
dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
// 第 j 次不持有:昨天就空着,或者今天刚卖(完成第 j 次交易)
dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
}
}

关卡 4: Stock V (反手做空)

这是最近非常火的一道「博主最爱」题。除了做多(期待涨),你还可以做空(期待跌)。状态从 2 个扩展到 3 个。

  • 状态 0:空仓。
  • 状态 1:持有多单(Long)。
  • 状态 2:持有空单(Short)。
1
2
3
// 平仓逻辑(不持有状态的来源)
dp[i][j][0] = Math.max(昨天空仓, 昨天持多今天卖, 昨天持空今天买回);
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1] + prices[i], dp[i-1][j][2] - prices[i]);

关卡 5: 冷冻期 (给市场一点冷静时间)

卖完后第二天不能买。这时候买入状态 dp[i][0] 只能从「大前天」的状态转移过来。

1
2
// 核心逻辑:买入必须跨过一天的冷冻
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2] - prices[i]); // 状态 2 是非冷冻期不持有

关卡 6: 手续费 (苍蝇腿也是肉)

由于每笔交易都要扣费,咱们只要在卖出的时候狠下心把钱减了就行。

1
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);

3. 蔚蓝的通关总结

在考场上,如果你脑子里乱成一团,请默念下面这几点:

  1. 状态定义第一层级:永远想「我手里有没有股票」。
  2. 写出三维数组:别急着空间优化,dp[天][次数][状态] 是最稳的。
  3. 避开 Integer.MIN_VALUE:别用它!用 -1e9 这种足够小的数,否则减去房价会溢出。
  4. 最后返回结果:一定是那些「不持有」状态的最大值。

算法不是为了难为人,而是为了让我们在面对复杂多变的情况时,依然能找到那条最清晰的逻辑线。

🥰 祝大家在这个「状态机」的世界里,都能买到最低位,卖在最高点!