Files
codetutor/backend/data/questions/best-time-to-buy-and-sell-stock-with-cooldown.yaml

213 lines
9.0 KiB
YAML

title: Best Time to Buy and Sell Stock with Cooldown
slug: best-time-to-buy-and-sell-stock-with-cooldown
difficulty: medium
leetcode_id: 309
leetcode_url: https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/
categories:
- arrays
- dynamic-programming
patterns:
- slug: dynamic-programming
is_optimal: true
function_signature: "def max_profit(prices: list[int]) -> int:"
test_cases:
visible:
- input: { prices: [1, 2, 3, 0, 2] }
expected: 3
- input: { prices: [1] }
expected: 0
hidden:
- input: { prices: [1, 2] }
expected: 1
- input: { prices: [2, 1] }
expected: 0
- input: { prices: [1, 2, 3, 4, 5] }
expected: 4
- input: { prices: [1, 2, 4, 2, 5, 7, 2, 4, 9, 0] }
expected: 13
- input: { prices: [6, 1, 6, 4, 3, 0, 2] }
expected: 7
- input: { prices: [3, 3, 3, 3] }
expected: 0
description: |
You are given an array `prices` where `prices[i]` is the price of a given stock on the i<sup>th</sup> day.
Find the maximum profit you can achieve. You may complete as many transactions as you like (i.e., buy one and sell one share of the stock multiple times) with the following restrictions:
- After you sell your stock, you cannot buy stock on the next day (i.e., cooldown one day).
**Note:** You may not engage in multiple transactions simultaneously (i.e., you must sell the stock before you buy again).
constraints: |
- `1 <= prices.length <= 5000`
- `0 <= prices[i] <= 1000`
examples:
- input: "prices = [1,2,3,0,2]"
output: "3"
explanation: "transactions = [buy, sell, cooldown, buy, sell]"
- input: "prices = [1]"
output: "0"
explanation: "Only one day available, no transaction possible."
explanation:
intuition: |
Think of this problem as a **state machine** where you're navigating between different states each day.
Imagine you're a trader who can be in one of three situations on any given day:
1. **Holding stock** — You own a share and are waiting for the right time to sell
2. **Just sold (cooldown)** — You just sold and must wait one day before buying again
3. **Ready to buy** — You don't own stock and can buy whenever you want
Each day, you transition between these states based on your action (buy, sell, or rest). The cooldown rule means that after selling, you're forced into the "cooldown" state before you can return to "ready to buy."
The key insight is that on each day, you only need to track the **maximum profit achievable** in each of these three states. By calculating how to transition optimally between states, you build up to the final answer.
This is a classic example of **state-based dynamic programming** — instead of tracking all possible transaction histories, we compress everything into a few meaningful states.
approach: |
We solve this using **State Machine Dynamic Programming**:
**Step 1: Define the states**
- `hold`: Maximum profit when holding a stock at the end of day `i`
- `sold`: Maximum profit when in cooldown (just sold) at the end of day `i`
- `rest`: Maximum profit when ready to buy (not holding, not in cooldown) at the end of day `i`
&nbsp;
**Step 2: Initialise the states**
- `hold = -prices[0]`: If we buy on day 0, our "profit" is negative the price
- `sold = 0`: Cannot have sold anything yet
- `rest = 0`: Starting with no stock and no profit
&nbsp;
**Step 3: Iterate and update states**
For each day `i` from 1 to `n-1`, calculate new states based on previous:
- `new_hold = max(hold, rest - prices[i])`: Either keep holding, or buy today (must come from rest)
- `new_sold = hold + prices[i]`: Sell today (must have been holding)
- `new_rest = max(rest, sold)`: Either stay resting, or transition from cooldown
&nbsp;
**Step 4: Return the answer**
- Return `max(sold, rest)` — the best profit when not holding stock
- We exclude `hold` because we want to have sold everything by the end
&nbsp;
The transitions ensure the cooldown constraint: to buy (`rest -> hold`), we must not have sold yesterday. The `sold` state forces a one-day gap before `rest` is updated.
common_pitfalls:
- title: Ignoring the Cooldown Constraint
description: |
A common mistake is to treat this like "Best Time to Buy and Sell Stock II" where you can buy and sell freely.
Without respecting cooldown, you might try to buy immediately after selling. For `prices = [1,2,3,0,2]`, buying on day 4 (price=0) right after selling on day 3 violates the rule.
The state machine approach naturally handles this — you can only buy from the `rest` state, which requires passing through `sold` first.
wrong_approach: "Greedy buy/sell without tracking cooldown"
correct_approach: "State machine with explicit cooldown state"
- title: Incorrect State Transitions
description: |
When updating states, you must use the **previous day's values**, not the current day's updated values.
For example, if you update `hold` first and then use that new `hold` value to calculate `sold`, you'll get wrong answers. The transitions should all be based on yesterday's state.
Use temporary variables or update all states simultaneously to avoid this bug.
wrong_approach: "Sequential updates using already-modified values"
correct_approach: "Simultaneous updates or temporary variables"
- title: Forgetting to Handle Single-Day Input
description: |
With only one price (e.g., `prices = [5]`), no transaction is possible. Your algorithm should return `0`.
Initialising `sold = 0` and `rest = 0` handles this — after the loop (which doesn't run), the max of `sold` and `rest` is `0`.
key_takeaways:
- "**State machine DP**: Model complex rules as states and transitions rather than tracking full history"
- "**Compress information**: Instead of remembering all past decisions, track only what matters for future choices"
- "**Stock problem family**: This pattern extends to problems with transaction fees, at most k transactions, or other constraints"
- "**Space optimisation**: Since we only need the previous day's states, we can use O(1) space instead of O(n)"
time_complexity: "O(n). We iterate through the prices array once, performing constant-time state updates at each step."
space_complexity: "O(1). We only maintain three variables (`hold`, `sold`, `rest`) regardless of input size."
solutions:
- approach_name: State Machine DP
is_optimal: true
code: |
def max_profit(prices: list[int]) -> int:
if not prices:
return 0
n = len(prices)
# Initial states on day 0
hold = -prices[0] # Bought on day 0
sold = 0 # Can't have sold yet
rest = 0 # No stock, no transactions
for i in range(1, n):
# Calculate new states based on previous day
# Use temp variables to avoid using updated values
new_hold = max(hold, rest - prices[i]) # Keep holding or buy today
new_sold = hold + prices[i] # Sell today
new_rest = max(rest, sold) # Stay resting or exit cooldown
# Update states for next iteration
hold, sold, rest = new_hold, new_sold, new_rest
# Return max profit when not holding stock
return max(sold, rest)
explanation: |
**Time Complexity:** O(n) — Single pass through the array.
**Space Complexity:** O(1) — Only three state variables used.
We model the problem as a state machine with three states. Each day, we compute the optimal way to transition into each state. The cooldown rule is enforced by requiring buys to come from `rest`, which can only be reached after passing through `sold`.
- approach_name: DP with Arrays
is_optimal: false
code: |
def max_profit(prices: list[int]) -> int:
if not prices:
return 0
n = len(prices)
# DP arrays for each state
hold = [0] * n # Max profit when holding stock
sold = [0] * n # Max profit in cooldown (just sold)
rest = [0] * n # Max profit when ready to buy
# Base case: day 0
hold[0] = -prices[0]
sold[0] = 0
rest[0] = 0
for i in range(1, n):
# Transition equations
hold[i] = max(hold[i-1], rest[i-1] - prices[i])
sold[i] = hold[i-1] + prices[i]
rest[i] = max(rest[i-1], sold[i-1])
# Best profit when not holding stock
return max(sold[n-1], rest[n-1])
explanation: |
**Time Complexity:** O(n) — Single pass through the array.
**Space Complexity:** O(n) — Three arrays of length n.
This version explicitly stores all states for each day, making the DP transitions clearer. While easier to understand and debug, it uses more memory than necessary. The space-optimised version above is preferred for production use.