状态机 DP 的本质是:在时间线推进的过程中,用有限个状态描述系统所处阶段,并在状态之间进行合法转移,求全局最优解。股票系列问题是状态机 DP 最经典、也最具迁移价值的模型。
一、什么是状态机 DP
不是“多维 DP”这么简单
状态机 DP 的核心特征
- 时间维度是线性的(天数 / 下标 i)
- 状态集合是有限且很小的(通常 2~5 个)
- 每一天的最优解,只依赖于:
- 前一天(或前几天)
- 合法状态之间的转移
典型形式:
1 | dp[i][state] = max / min { |
为什么股票问题天然适合状态机 DP?
因为每天的行为是有限的:
- 买
- 卖
- 什么都不做
而这些行为,本质上只会改变“是否持有股票”这一状态。
二、股票问题的统一状态定义
不管题目怎么变,本质都围绕这两个基础状态:
1 | state = 0 → 当前不持有股票 |
因此最基础的定义是:
1 | dp[i][j] 表示:第 i 天结束后,处于状态 j 时的最大利润 |
三、基础模型:122 买卖股票的最佳时机 II(无限次交易)
状态定义
1 | dp[i][0]:第 i 天结束后,不持股的最大利润 |
状态转移(状态机视角)
不持股(state = 0)
- 昨天就不持股,今天什么都不做
- 昨天持股,今天卖出
1 | dp[i][0] = max( |
持股(state = 1)
- 昨天就持股,今天继续持有
- 昨天不持股,今天买入
1 | dp[i][1] = max( |
这是最纯粹的状态机 DP 模型,后续所有变形都在此基础上增加约束。
四、加入约束一:冷冻期(309)
买入操作不再只依赖前一天。
卖出后,下一天不能立刻买入,因此:
- 买入只能从 前两天的不持股状态 转移
状态转移变化
1 | dp[i][0] = max( |
这类题的本质不是“新增状态”,而是:转移来源发生了变化
五、加入约束二:交易次数限制(188 / 123)
为什么要引入「交易次数」维度?
因为一次完整交易 = 买入 + 卖出
限制交易次数,意味着 dp 数组中必须记录“还剩几次卖出的机会”。
状态定义(标准三维状态机)
1 | dp[i][k][j] |
核心转移逻辑(只在卖出时消耗交易次数)
1 | dp[i][k][0] = max( |
为什么是卖出时 k-1,而不是买入?
因为一次交易以“完成卖出”为标志。理论上由于最后一天不持有股票的价值一定最高,所以买入时进行 k-1 也可以。
121 / 123 与 188 的关系
| 题目 | 本质 |
|---|---|
| 121 | k = 1 的特例(可用贪心优化) |
| 123 | k = 2 的特例 |
| 188 | 任意 k 的通用模型 |
六、加入约束三:手续费(714)
本质变化
每一笔交易都会多付出一个固定成本。
手续费只影响 买入或卖出其中一个时机,常见处理方式:
在买入时扣手续费
1 | dp[i][1] = max( |
或在卖出时扣手续费(等价)
1 | dp[i][0] = max( |
七、状态机 DP 的工程级总结
建模顺序
- 时间轴是什么?(天数 / 下标)
- 系统有哪些有限状态?(股票的持有与否)
- 哪些状态之间可以互相转移?(持有股票通过当天卖出转化为未持有)
- 哪些转移有额外约束?(手续费、交易次数限制)
股票状态机的通用模板
1 | for i in [0..n): |
状态机 DP 变形
- 可迁移到时机开发中:订单状态流转、流控 / 限流、工作流引擎、资源占用 / 释放建模
这是后端系统里“时间 + 状态”的通用建模方式。
相关代码
本文涉及的所有代码与笔记,均已同步至我的 GitHub 算法仓库,作为 Java 后端校招过程中的学习记录。