Algorithm

Stock with Cooldown

Dynamic Programming Pattern

Best Time to Buy and Sell Stock with Cooldown

You are given an array prices where prices[i] is the stock price on day i. You may complete as many transactions as you like, but you may hold at most one share at a time — you must sell before buying again. There is one extra rule: after you sell, you must skip the following day entirely and cannot buy on it. Return the maximum profit achievable. Doing nothing is allowed, so the answer is never negative.

CONSTRAINTS
  • 1 ≤ prices.length ≤ 5000
  • 0 ≤ prices[i] ≤ 1000
  • You may hold at most one share at any moment
  • After a sale on day i, the earliest possible buy is day i + 2
EXAMPLE 1
Input: prices = [1, 2, 3, 0, 2]
Output: 3
Buy at 1 and sell at 2 for a profit of 1, rest on the next day as required, then buy at 0 and sell at 2 for a profit of 2.
EXAMPLE 2
Input: prices = [1]
Output: 0
A single day leaves no later day on which to sell, so no transaction can be completed.
EXAMPLE 3
Input: prices = [1, 2, 3, 4, 5]
Output: 4
One long hold from 1 to 5 earns 4. Splitting the climb into separate daily trades would trigger a rest day after each sale and earn less.
EXAMPLE 4
Input: prices = [2, 1, 4]
Output: 3
Buying at 1 and selling at 4 earns 3. The first day's price of 2 is simply skipped, which costs nothing.
EXAMPLE 5
Input: prices = [6, 1, 3, 2, 4, 7]
Output: 6
Buy at 1 and sell at 3 for 2, rest through the price of 2, then buy at 4 and sell at 7 for 3 — five in total — is beaten by holding from 1 all the way to 7 for 6, since the cooldown makes the split plan lose access to the cheapest re-entry.
How long is the cooldown, and does it apply after buying as well?
One day, and only after selling. A sale on day i means the earliest legal purchase is day i + 2. Buying carries no penalty at all.
Can I hold more than one share at a time?
No — you must sell before buying again. This is what keeps the number of modes small; allowing several shares would require the count of shares held to become part of the state.
Is there a limit on the number of transactions?
No limit here, which is why the state needs only a mode and not a transaction counter. If the problem capped transactions at k, the state would gain a third component and the running time would become proportional to N times k.
What happens if I am still holding a share on the last day?
It is worth nothing — profit is only realised on a sale. The base case returns zero for every mode past the final day, which correctly makes an unsold share pointless.
Can the answer be negative?
No. Doing nothing is always available and earns zero, so the maximum is at least zero on every input.
Why the simple trick stops working

With unlimited transactions and no cooldown, this problem is famously easy: add up every upward move, because any rise from day i to day i+1 can be captured by buying and selling on consecutive days, and a long climb is the sum of its daily steps. On [1, 2, 3] you would collect (2−1) + (3−2) = 2, the same as buying at 1 and selling at 3.

The cooldown destroys that argument. Chopping one long hold into many small trades now costs you a forced rest day after each sale, and those lost days can be more valuable than the flexibility. So the two are no longer equivalent, and we have to decide, day by day, whether to trade — which is exactly what these tables are for.

Also worth stating precisely: the cooldown applies after selling only. Buying carries no penalty. And the rest day is a single day, so a sale on day i permits a purchase from day i+2 onward.

The state, as a story

Say I am standing on day i. What do I actually need to know to decide what to do?

- If I want to sell today, I must already be holding a share.
- If I want to buy today, I must not be holding one.

So besides the day, the one thing that decides my options is: am I holding a share right now? That single yes/no is the rest of the state.

State Definitionf(i, holding) is the most profit I can still make from day i to the end, where holding says whether I already own a share as day i begins.
The choices

Two cases, each with two options.

If I am holding a share, I can:
- sell today → collect prices[i], and the cooldown kicks in, so the next day I may act is i + 2: prices[i] + f(i+2, not holding).
- keep holding → do nothing, still holding tomorrow: f(i+1, holding).

If I am not holding, I can:
- buy today → pay prices[i] and now I hold a share: -prices[i] + f(i+1, holding).
- do nothing → still empty-handed tomorrow: f(i+1, not holding).

Take the better option in each case:

f(i, holding) = max( prices[i] + f(i+2, not holding), f(i+1, holding) )
f(i, not holding) = max( −prices[i] + f(i+1, holding), f(i+1, not holding) )

The whole cooldown rule lives in one spot: the i+2 after a sale. Buying and waiting just step to i+1.

Buying subtracts the price and selling adds it, so the running total is real cash — no need to remember what you paid.

Base case: once i runs past the last day (i >= n), nothing is left to earn, so f = 0. A share still held at the end earns nothing, which correctly rules out buying and never selling.

The answer is f(0, not holding) — day 0, nothing owned yet.

The three versions

Memoised, straight from the story. holding is a boolean:

python
def max_profit(prices):
    n = len(prices)
    memo = {}

    def f(i, holding):
        if i >= n: return 0
        if (i, holding) in memo: return memo[(i, holding)]
        if holding:
            best = max(prices[i] + f(i + 2, False),   # sell -> cooldown -> skip to i+2
                       f(i + 1, True))                 # keep holding
        else:
            best = max(-prices[i] + f(i + 1, True),    # buy today
                       f(i + 1, False))                # do nothing
        memo[(i, holding)] = best
        return best

    return f(0, False)

There are n days × 2 holding-values = 2n states, constant work each, so O(N) time.

Now the table. Let dp[i][h] hold what f(i, h) returned. Each recurrence line becomes an array assignment. The reads reach forward — to i+1 and i+2 — so fill from the last day backwards. Give the table two extra rows so the i+2 read is always valid and lands on the base value 0:

python
def max_profit(prices):
    n = len(prices)
    dp = [[0, 0] for _ in range(n + 2)]      # dp[i][0] = not holding, dp[i][1] = holding

    for i in range(n - 1, -1, -1):
        dp[i][1] = max(prices[i] + dp[i + 2][0], dp[i + 1][1])   # holding: sell or keep
        dp[i][0] = max(-prices[i] + dp[i + 1][1], dp[i + 1][0])  # empty: buy or wait

    return dp[0][0]

And since each day reads only one and two days ahead, we can keep just those two rows instead of the whole table:

python
def max_profit(prices):
    n = len(prices)
    ahead1 = [0, 0]        # dp[i+1]
    ahead2 = [0, 0]        # dp[i+2]
    for i in range(n - 1, -1, -1):
        cur = [0, 0]
        cur[1] = max(prices[i] + ahead2[0], ahead1[1])
        cur[0] = max(-prices[i] + ahead1[1], ahead1[0])
        ahead2, ahead1 = ahead1, cur
    return ahead1[0]

O(1) space.

Worked Example:prices = [1, 2, 3, 0, 2]
Filling backwards. Write each day as [not holding, holding]. Days at or past the end are [0, 0].
- Day 4, price 2: holding → max(2 + dp[6][0]=0, dp[5][1]=0) = 2 (sell for 2). not holding → max(−2 + 0, 0) = 0. → [0, 2]
- Day 3, price 0: holding → max(0 + dp[5][0]=0, dp[4][1]=2) = 2. not holding → max(−0 + dp[4][1]=2, dp[4][0]=0) = 2 (buy for free, sell tomorrow). → [2, 2]
- Day 2, price 3: holding → max(3 + dp[4][0]=0, dp[3][1]=2) = 3 (sell for 3). not holding → max(−3 + dp[3][1]=2, dp[3][0]=2) = 2. → [2, 3]
- Day 1, price 2: holding → max(2 + dp[3][0]=2, dp[2][1]=3) = 4. not holding → max(−2 + dp[2][1]=3, dp[2][0]=2) = 2. → [2, 4]
- Day 0, price 1: not holding → max(−1 + dp[1][1]=4, dp[1][0]=2) = 3 (buy at 1). holding → max(1 + dp[2][0]=2, dp[1][1]=4) = 4. → [3, 4]

Answer dp[0][0] = 3: buy at 1, sell at 2 (profit 1), cooldown, buy at 0, sell at 2 (profit 2).

Notice what the table rejected. Buying at 1 and selling at 3 on day 2 earns 2, but the forced rest on day 3 misses the price of 0, and you cannot do better after — total 2. The cooldown genuinely changed the best plan.

The pattern this belongs to

The move that cracked this is worth keeping: when the day alone is not enough, ask what else decides your options, and add exactly that to the state. Here it was one yes/no — am I holding a share? Two situations on the same day with different holdings face different futures, so the holding must be in the state.

And notice how the cooldown was handled: not with a new "resting" state, but as a jump in the index — a sale sends you to i+2 instead of i+1. A "you must wait k steps after doing X" rule almost always becomes a longer jump like this.

The rest of the stock family follows the same recipe. A transaction fee subtracts a constant on the sell step. "At most k transactions" adds a counter, making the state (day, holding, transactions left). In every case the state grows by exactly what the new rule cares about, and nothing else.

Exponential Try Every Schedule
O(N) Time · O(N) Space Memoisation
O(N) Time · O(N) Space Tabulation
O(N) Time · O(1) Space Rolling Rows