LeetCode #309 Medium

Best Time to Buy and Sell Stock with Cooldown

Max profit from unlimited trades where a sell forces one cooldown day before the next buy.

dynamic-programmingarraystate-machine
Open on LeetCode ↗
02

Intuition

The tempting shortcut is to track one number, the max profit reachable by day i, and update it greedily. That collapses two different situations into one: whether you are allowed to buy today depends on whether you sold yesterday, and a single running max cannot see that. You need three parallel states per day instead: holding a share, having just sold today, and resting (own nothing, did not just sell). The cooldown rule becomes a single missing transition: rest can only be entered from yesterday's rest or yesterday's sold, and hold can only buy out of rest, never out of a fresh sold. That one forbidden edge is the entire cooldown constraint, encoded structurally instead of checked with an if.

How to spot this pattern

Three states per day — holding, just sold, resting — with transitions between them. The cooldown is expressed structurally: you can only buy from rest, and sold must pass through rest before buying again. State machines handle constraints that a single running variable can't.

03

Approach

1

Define three rolling states

Keep hold (best profit while currently owning a share), sold (best profit having sold exactly today), and rest (best profit owning nothing and not having sold today). Initialize hold to -prices[0] since day 0's only option is to buy, and sold/rest to 0/-infinity respectively.

2

Transition day by day

Each day, the new hold is the better of keeping yesterday's share or buying today out of yesterday's rest state. The new sold is always yesterday's hold plus today's price, since selling requires having held. The new rest is the better of staying resting or coming off cooldown from yesterday's sold. Compute all three from the previous day's values before overwriting, so nothing reads an already-updated state.

3

Read off the answer

The optimal ending state can never be hold, since an unsold share is profit left on the table. The answer is the maximum of the final sold and rest values, both of which represent ending the timeline owning nothing.

04

Solution & live demo

1class Solution:
2 def maxProfit(self, prices: list[int]) -> int:
3 n = len(prices)
4 if n < 2:
5 return 0
6 hold, sold, rest = -prices[0], float('-inf'), 0
7 for i in range(1, n):
8 new_hold = max(hold, rest - prices[i])
9 new_sold = hold + prices[i]
10 new_rest = max(rest, sold)
11 hold, sold, rest = new_hold, new_sold, new_rest
12 return max(sold, rest)
05

Common pitfalls

Updating the states sequentially

✗ Wrong
hold = max(hold, rest - prices[i])
sold = hold + prices[i]
✓ Right
new_hold = max(hold, rest - prices[i])
new_sold = hold + prices[i]
...
hold, sold, rest = new_hold, new_sold, new_rest

The second line would use today's hold rather than yesterday's, allowing a buy and sell on the same day. All three transitions must read the previous day's values.

Buying from the sold state

✗ Wrong
new_hold = max(hold, sold - prices[i])
✓ Right
new_hold = max(hold, rest - prices[i])

That's precisely the cooldown violation — buying the day after selling. Routing purchases through rest forces the mandatory idle day between a sale and the next purchase.

Returning hold in the final answer

✗ Wrong
return max(hold, sold, rest)
✓ Right
return max(sold, rest)

Ending while still holding a share means the money is tied up in stock, not realised as profit. Only the two cash states are valid endings.

06

Edge cases

Single price / empty array

With fewer than 2 days there is no possible trade, so both sold and rest stay at 0 and the answer is 0.

Prices strictly decreasing

Every buy loses money, so hold never contributes a positive update; sold and rest both settle at 0, correctly reporting no profitable trade exists.

Two-day rise then immediate cooldown-forced wait

After a sell, the very next buy attempt out of sold is structurally impossible since buys only draw from rest, which naturally enforces the one-day gap.

Alternating up/down prices

The running max in each state means a locally bad day never destroys a previously found good state; hold, sold, and rest each just keep their best value seen so far.

07

Complexity

Time
O(n)
Space
O(1)
Three rolling scalars replace what would otherwise be a 3 x n table.