题目描述
思路解析
一句话答案:LeetCode 309 最佳买卖股票时机含冷冻期用状态机动态规划求解:把每天收盘后的处境拆成持股 hold、刚卖出 sold、空仓可买 rest 三个状态,冷冻期通过「买入只能从 rest 转来、sold 必须先歇一天变 rest」编码进转移里,答案取最后一天 max(sold, rest)。时间 O(n)、空间 O(1)。
含冷冻期的股票题在问什么
prices[i] 是第 i 天的股价,可以做任意多次交易,但手里同时最多持有一股,而且卖出后有一天冷冻期——隔一天才能再买。求能拿到的最大利润。和普通的多次交易股票题相比,唯一的新约束就是这个冷冻期:它让「今天卖、明天买」这种衔接非法,昨天发生了什么开始影响今天能做什么。
为什么贪心不够用,要上状态机 DP
没有冷冻期时(LeetCode 122),逢涨就吃是可行的贪心:每一段上涨都能完整收进口袋。冷冻期打破了这一点——吃下一段小涨幅可能触发冷冻,错过紧随其后的大涨幅,局部最优不再等于全局最优,得权衡「现在卖」和「憋一手」。
权衡类问题的通用出路是把「处境」显式建模。观察发现,无论历史操作序列多复杂,第 i 天收盘后你的处境只有三种:手里有股、今天刚卖掉、空仓且不在冷冻期。只要记住每种处境下的最大利润,明天的决策就与更早的历史无关——这就是无后效性,也是状态机动态规划的出发点。
hold、sold、rest 三个状态怎么定义
hold 表示当天结束时手里持有一股的最大利润;sold 表示当天恰好卖出、正要进入冷冻期的最大利润;rest 表示当天结束时空仓、且明天可以自由买入的最大利润。第 0 天初始化:买入则 hold = -prices[0](利润先垫掉成本);rest = 0;sold 设为负无穷——第 0 天没有股可卖,这个状态不可达,设成 0 会让不可能的路径混进比较。
为什么必须拆三个状态、两个不够?因为冷冻期要求「刚卖出」和「空仓可买」被区分开:同样是没有股票,刚卖出的人明天不能买,歇过一天的人可以。把这两种空仓合并,冷冻期就没地方表达了。
冷冻期到底藏在转移方程的哪一步
三条转移:hold = max(hold, rest - p),继续持有,或从空仓花 p 买入;sold = hold + p,把手里那股以价 p 卖掉;rest = max(rest, sold),继续空着,或昨天刚卖完的人歇满一天转成可买。
冷冻期没有单独的 if 判断,它整个编码在「买入只能接 rest」这一条里:想买必须处于 rest,而 rest 要么一直空仓、要么由昨天的 sold 转化而来——从卖出到再买入天然隔了至少一天。转移图本身就是规则,这是状态机 DP 最漂亮的地方。
答案为什么取 max(sold, rest),复杂度多少
最后一天结束时,理性的收尾不该还持着股票:股票没卖出就没有落袋,hold 里垫着的买入成本收不回来,所以答案在两个空仓状态里取较大值 max(sold, rest)。以 prices=[1,2,3,0,2,4,1,3,2] 为例,递推到最后得到的最大利润是 8。
时间复杂度 O(n),一遍线性递推;每天只依赖前一天的三个值,用三个滚动变量即可,空间 O(1)。易错点集中在初始化和收尾:sold 初值忘写负无穷、答案误取 hold,都是这道题的经典翻车现场。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
关键就这三种状态,看懂转移就通了。
冷冻期就藏在这里:买入(hold)只能接 rest,而 rest 要么一直空、要么是昨天 sold 过来——隔了一天。
第 0 天三状态初始化:买入则 hold=-1(花掉 1);还没买过所以 sold 不可达=−∞;空仓利润 rest=0。
第 1 天(价 2):持股可继续持(-1)或从空仓买入(0−2);卖出 = 昨天持股-1+2;空仓 = 保持(0)或昨天刚卖完转过来(−∞)。
三状态各取最优落子:hold=-1,sold=1,rest=0。
第 2 天(价 3):持股可继续持(-1)或从空仓买入(0−3);卖出 = 昨天持股-1+3;空仓 = 保持(0)或昨天刚卖完转过来(1)。
三状态各取最优落子:hold=-1,sold=2,rest=1。
第 3 天(价 0):持股可继续持(-1)或从空仓买入(1−0);卖出 = 昨天持股-1+0;空仓 = 保持(1)或昨天刚卖完转过来(2)。
三状态各取最优落子:hold=1,sold=-1,rest=2。
第 4 天(价 2):持股可继续持(1)或从空仓买入(2−2);卖出 = 昨天持股1+2;空仓 = 保持(2)或昨天刚卖完转过来(-1)。
三状态各取最优落子:hold=1,sold=3,rest=2。
第 5 天(价 4):持股可继续持(1)或从空仓买入(2−4);卖出 = 昨天持股1+4;空仓 = 保持(2)或昨天刚卖完转过来(3)。
三状态各取最优落子:hold=1,sold=5,rest=3。
第 6 天(价 1):持股可继续持(1)或从空仓买入(3−1);卖出 = 昨天持股1+1;空仓 = 保持(3)或昨天刚卖完转过来(5)。
三状态各取最优落子:hold=2,sold=2,rest=5。
第 7 天(价 3):持股可继续持(2)或从空仓买入(5−3);卖出 = 昨天持股2+3;空仓 = 保持(5)或昨天刚卖完转过来(2)。
三状态各取最优落子:hold=2,sold=5,rest=5。
第 8 天(价 2):持股可继续持(2)或从空仓买入(5−2);卖出 = 昨天持股2+2;空仓 = 保持(5)或昨天刚卖完转过来(5)。
三状态各取最优落子:hold=3,sold=4,rest=5。
第 9 天(价 5):持股可继续持(3)或从空仓买入(5−5);卖出 = 昨天持股3+5;空仓 = 保持(5)或昨天刚卖完转过来(4)。
三状态各取最优落子:hold=3,sold=8,rest=5。
最后一天手里不该还留着股(持股=没卖=没落袋),所以答案取 sold 与 rest 的较大值 = 8。
边界先想清。
两个高频追问。
参考代码
def maxProfit(prices): hold, sold, rest = -prices[0], float("-inf"), 0 for p in prices[1:]: hold, sold, rest = max(hold, rest - p), hold + p, max(rest, sold) return max(sold, rest)复杂度
- 时间:O(n),一遍线性递推
- 空间:O(1),三个滚动变量
易错点
面试追问把动画讲成自己的话
追问没有冷冻期(LC122)怎么变?
追问为什么要拆三个状态而不是两个?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
零钱兑换 II
LeetCode 518 · 中等 · 沿着 二维动态规划 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题