A clear explanation of Best Time to Buy and Sell Stock with Cooldown using dynamic programming states.
Problem Restatement
We are given an integer array prices.
prices[i] is the price of a stock on day i.
We may complete as many transactions as we want, but with two restrictions:
- We cannot hold more than one stock at the same time.
- After selling a stock, we cannot buy on the next day. That next day is a cooldown day.
Return the maximum profit we can make.
The official constraints are 1 <= prices.length <= 5000 and 0 <= prices[i] <= 1000. The problem is tagged as Array and Dynamic Programming.
Input and Output
| Item | Meaning |
|---|---|
| Input | Integer array prices |
| Output | Maximum possible profit |
| Buy rule | Can buy only when not holding stock |
| Sell rule | Can sell only when holding stock |
| Cooldown rule | After selling, cannot buy on the next day |
Function shape:
def maxProfit(prices: list[int]) -> int:
...Examples
Example 1:
prices = [1, 2, 3, 0, 2]Output:
3One optimal sequence is:
buy, sell, cooldown, buy, sellThis means:
| Day | Price | Action | Profit Change |
|---|---|---|---|
| 0 | 1 | Buy | -1 |
| 1 | 2 | Sell | +2 |
| 2 | 3 | Cooldown | 0 |
| 3 | 0 | Buy | 0 |
| 4 | 2 | Sell | +2 |
Total profit:
-1 + 2 + 0 + 0 + 2 = 3Example 2:
prices = [1]Output:
0There is only one day, so no sell can happen after a buy.
First Thought: Try Every Choice
On each day, we may have several choices:
| State | Possible Actions |
|---|---|
| Not holding stock | Buy or skip |
| Holding stock | Sell or keep holding |
| Just sold yesterday | Cooldown, so cannot buy today |
A recursive solution can try all valid choices.
But without memoization, the same subproblems repeat many times.
For example, many different action paths may lead to:
day 4, not holding stockSo plain recursion becomes exponential.
We need dynamic programming.
Key Insight
The decision on each day depends only on our current state.
We do not need the full transaction history.
We only need to know the best profit after each day in these states:
| State | Meaning |
|---|---|
hold | Maximum profit after today while holding one stock |
sold | Maximum profit after today if we sold today |
rest | Maximum profit after today while not holding stock and not selling today |
These three states encode the cooldown rule.
If we sell today, tomorrow we cannot buy. That state is represented by sold.
If we are in rest, we are free to buy.
State Transitions
For each price p, compute the next states.
Holding stock
At the end of today, we hold stock in one of two ways:
- We already held stock yesterday.
- We were resting yesterday and buy today.
new_hold = max(hold, rest - p)We cannot buy from sold, because that would mean buying immediately after selling yesterday.
Sold today
At the end of today, we are in sold only if we held stock yesterday and sell today.
new_sold = hold + pResting
At the end of today, we are resting if:
- We were already resting yesterday.
- We sold yesterday and today is the cooldown day.
new_rest = max(rest, sold)These three transitions enforce all rules.
Algorithm
Initialize:
hold = -infinity
sold = -infinity
rest = 0Before any day:
| State | Value | Reason |
|---|---|---|
hold | -infinity | Cannot be holding before buying |
sold | -infinity | Cannot have sold before any day |
rest | 0 | Doing nothing gives zero profit |
For each price:
- Compute
new_hold. - Compute
new_sold. - Compute
new_rest. - Replace old states with new states.
At the end, return:
max(sold, rest)We do not return hold, because holding a stock at the end means we spent money on an unsold stock. Selling it would be required to realize profit.
Correctness
For each day, the three DP states represent the best possible profit among all valid action sequences ending in that state.
The initialization is correct because before trading starts, the only possible state is resting with profit 0.
Assume the states are correct after yesterday.
For today:
new_hold is correct because the only ways to end today holding stock are to keep holding from yesterday, or buy today from a resting state. Buying from yesterday’s sold state is forbidden by the cooldown rule.
new_sold is correct because the only way to sell today is to have held stock yesterday.
new_rest is correct because we can rest after already resting, or spend today as the cooldown day after selling yesterday.
These cases cover every valid action and exclude every invalid action. Therefore, the states remain correct after every day.
At the end, the best realized profit must be in sold or rest, because holding a stock means the last buy has not been closed by a sell. Thus max(sold, rest) is the maximum possible profit.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | We process each price once |
| Space | O(1) | We store only three states |
Implementation
class Solution:
def maxProfit(self, prices: list[int]) -> int:
hold = float("-inf")
sold = float("-inf")
rest = 0
for price in prices:
new_hold = max(hold, rest - price)
new_sold = hold + price
new_rest = max(rest, sold)
hold = new_hold
sold = new_sold
rest = new_rest
return max(sold, rest)Code Explanation
We start with three states:
hold = float("-inf")
sold = float("-inf")
rest = 0rest = 0 means we have done nothing and made no profit.
hold and sold are impossible before processing any day, so they start as negative infinity.
For each price:
for price in prices:Compute the next holding state:
new_hold = max(hold, rest - price)This means either keep holding, or buy today from a free resting state.
Compute the next sold state:
new_sold = hold + priceThis means sell the stock we held before today.
Compute the next rest state:
new_rest = max(rest, sold)This means either continue resting, or enter cooldown after selling yesterday.
Then update all states together:
hold = new_hold
sold = new_sold
rest = new_restUpdating together matters. Each new state must be based on yesterday’s states, not partially updated values from today.
Finally:
return max(sold, rest)This returns the best profit when we finish without holding stock.
Testing
def run_tests():
s = Solution()
assert s.maxProfit([1, 2, 3, 0, 2]) == 3
assert s.maxProfit([1]) == 0
assert s.maxProfit([1, 2]) == 1
assert s.maxProfit([2, 1]) == 0
assert s.maxProfit([1, 2, 3]) == 2
assert s.maxProfit([6, 1, 6, 4, 3, 0, 2]) == 7
assert s.maxProfit([0, 0, 0]) == 0
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
[1, 2, 3, 0, 2] | Standard cooldown example |
[1] | No complete transaction possible |
[1, 2] | One simple profitable trade |
[2, 1] | No profitable trade |
[1, 2, 3] | Best is buy once and sell once |
[6, 1, 6, 4, 3, 0, 2] | Cooldown affects later buying |
[0, 0, 0] | Zero-price edge case |