title: Best Time to Buy and Sell Stock IV
slug: best-time-to-buy-and-sell-stock-iv
difficulty: hard
leetcode_id: 188
leetcode_url: https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/
categories:
- arrays
- dynamic-programming
patterns:
- dynamic-programming
function_signature: "def max_profit(k: int, prices: list[int]) -> int:"
test_cases:
visible:
- input: { k: 2, prices: [2, 4, 1] }
expected: 2
- input: { k: 2, prices: [3, 2, 6, 5, 0, 3] }
expected: 7
hidden:
- input: { k: 1, prices: [1, 2, 3, 4, 5] }
expected: 4
- input: { k: 0, prices: [1, 2, 3] }
expected: 0
- input: { k: 2, prices: [1] }
expected: 0
- input: { k: 100, prices: [1, 2, 3, 4, 5] }
expected: 4
- input: { k: 2, prices: [5, 4, 3, 2, 1] }
expected: 0
- input: { k: 3, prices: [1, 2, 4, 2, 5, 7, 2, 4, 9, 0] }
expected: 15
description: |
You are given an integer array `prices` where `prices[i]` is the price of a given stock on the ith day, and an integer `k`.
Find the maximum profit you can achieve. You may complete **at most `k` transactions**: i.e., you may buy at most `k` times and sell at most `k` times.
**Note:** You may not engage in multiple transactions simultaneously (i.e., you must sell the stock before you buy again).
constraints: |
- `1 <= k <= 100`
- `1 <= prices.length <= 1000`
- `0 <= prices[i] <= 1000`
examples:
- input: "k = 2, prices = [2,4,1]"
output: "2"
explanation: "Buy on day 1 (price = 2) and sell on day 2 (price = 4), profit = 4 - 2 = 2."
- input: "k = 2, prices = [3,2,6,5,0,3]"
output: "7"
explanation: "Buy on day 2 (price = 2) and sell on day 3 (price = 6), profit = 6 - 2 = 4. Then buy on day 5 (price = 0) and sell on day 6 (price = 3), profit = 3 - 0 = 3. Total profit = 4 + 3 = 7."
explanation:
intuition: |
Imagine you're a trader who can make at most `k` round-trip trades (buy then sell). Each day you look at the stock price and must decide: should I buy, sell, or hold? The catch is that you can only hold one stock at a time and have limited transactions.
Think of it like having `k` "transaction slots". Each slot can capture one buy-sell pair. Your goal is to fill these slots with the most profitable trades, but trades can't overlap — you must complete one before starting another.
The key insight is that on any given day, your state depends on two things: **how many transactions you've completed** and **whether you currently hold a stock**. This naturally leads to a DP formulation where we track the maximum profit for each possible state.
For each of the `k` transactions, we track two values:
- The maximum profit if we're currently **holding** a stock (bought but not yet sold)
- The maximum profit if we're **not holding** a stock (either haven't bought or already sold)
By updating these states as we walk through each day, we can find the optimal profit using all available transactions.
approach: |
We solve this using **Dynamic Programming with State Tracking**:
**Step 1: Handle the edge case**
- If `k >= n // 2` where `n` is the number of days, we can make as many transactions as we want (unlimited transactions)
- In this case, simply sum all profitable price increases: add `prices[i] - prices[i-1]` whenever it's positive
- This optimisation prevents memory issues when `k` is very large
**Step 2: Initialise state arrays**
- Create arrays `buy[k+1]` and `sell[k+1]` for tracking profit at each transaction count
- `buy[i]`: Maximum profit when we've completed `i-1` sells and are currently holding a stock
- `sell[i]`: Maximum profit when we've completed `i` full transactions (not holding)
- Initialise `buy[i] = -infinity` (impossible state initially) and `sell[i] = 0`
**Step 3: Process each day**
- For each price, update states from transaction `k` down to `1`:
- `buy[j] = max(buy[j], sell[j-1] - price)` — either keep holding, or buy today using profit from `j-1` sells
- `sell[j] = max(sell[j], buy[j] + price)` — either stay sold, or sell today
**Step 4: Return the result**
- Return `sell[k]` — maximum profit after at most `k` complete transactions
This approach works because we're building up the optimal solution for each number of transactions, ensuring we never miss a better combination.
common_pitfalls:
- title: Ignoring the Large k Optimisation
description: |
When `k` is very large (e.g., `k >= n/2`), creating arrays of size `k` can cause memory issues or TLE.
If you can make `n/2` or more transactions on `n` days, you can effectively capture every upward price movement. In this case, simplify to the "unlimited transactions" problem: sum all positive differences.
wrong_approach: "Always use full DP regardless of k size"
correct_approach: "Check if k >= n/2 and use greedy sum for large k"
- title: Wrong State Transition Order
description: |
When updating states, you must be careful about the order of operations. If you update `sell[j]` before `buy[j]` in the same iteration, or process transactions in the wrong order, you might use stale values.
The safest approach is to iterate transactions from `k` down to `1`, ensuring each state update uses values from the previous day.
wrong_approach: "Update buy and sell in arbitrary order"
correct_approach: "Process transactions in reverse order (k to 1)"
- title: Off-by-One in Transaction Counting
description: |
A "transaction" consists of a buy AND a sell. It's easy to confuse:
- Number of buys allowed
- Number of sells allowed
- Number of complete buy-sell pairs
Be clear that `k` transactions means `k` complete round trips. After the kth sell, no more buying is allowed.
wrong_approach: "Treating k as number of buys OR sells separately"
correct_approach: "k is the number of complete buy-sell pairs"
- title: Not Handling Empty or Single-Day Arrays
description: |
With only one day, no transaction can complete (you need at least 2 days to buy then sell). Similarly, if `k = 0`, no transactions are allowed.
Both cases should return `0` profit.
wrong_approach: "Not checking for trivial edge cases"
correct_approach: "Return 0 if n < 2 or k == 0"
key_takeaways:
- "**State machine DP**: Model the problem as states (holding/not holding × transaction count) with transitions"
- "**Optimisation for large k**: When transactions are effectively unlimited, fall back to the simpler greedy approach"
- "**Generalisation of stock problems**: This is the most general single-stock problem — simpler variants (I, II, III) are special cases"
- "**Space optimisation**: We only need the previous day's states, reducing space from O(n×k) to O(k)"
time_complexity: "O(n × k). For each of the `n` prices, we update `k` transaction states. With the large-k optimisation, it's O(n) when `k >= n/2`."
space_complexity: "O(k). We maintain two arrays of size `k+1` for tracking buy and sell states."
solutions:
- approach_name: Dynamic Programming with State Arrays
is_optimal: true
code: |
def max_profit(k: int, prices: list[int]) -> int:
n = len(prices)
if n < 2 or k == 0:
return 0
# Optimisation: if k >= n/2, we can capture all gains (unlimited transactions)
if k >= n // 2:
profit = 0
for i in range(1, n):
# Add every upward price movement
if prices[i] > prices[i - 1]:
profit += prices[i] - prices[i - 1]
return profit
# buy[j] = max profit holding a stock, having completed j-1 sells
# sell[j] = max profit not holding, having completed j sells
buy = [float('-inf')] * (k + 1)
sell = [0] * (k + 1)
for price in prices:
# Update from k down to 1 to avoid using today's values
for j in range(k, 0, -1):
# Option: sell today (complete transaction j)
sell[j] = max(sell[j], buy[j] + price)
# Option: buy today (start transaction j)
buy[j] = max(buy[j], sell[j - 1] - price)
return sell[k]
explanation: |
**Time Complexity:** O(n × k) — For each price, we update k states. O(n) when k >= n/2.
**Space Complexity:** O(k) — Two arrays of size k+1.
We track the best profit for each transaction count, distinguishing between holding and not holding a stock. The reverse iteration (k to 1) ensures we use consistent state values within each day.
- approach_name: 2D DP Table
is_optimal: false
code: |
def max_profit(k: int, prices: list[int]) -> int:
n = len(prices)
if n < 2 or k == 0:
return 0
# Optimisation for large k
if k >= n // 2:
return sum(max(0, prices[i] - prices[i - 1]) for i in range(1, n))
# dp[i][j] = max profit using at most j transactions up to day i
dp = [[0] * (k + 1) for _ in range(n)]
for j in range(1, k + 1):
# Track best profit if we bought on some previous day
max_diff = -prices[0]
for i in range(1, n):
# Either don't trade on day i, or sell on day i
dp[i][j] = max(dp[i - 1][j], prices[i] + max_diff)
# Update best buy point: buy on day i using profit from j-1 transactions
max_diff = max(max_diff, dp[i][j - 1] - prices[i])
return dp[n - 1][k]
explanation: |
**Time Complexity:** O(n × k) — Nested loops over days and transactions.
**Space Complexity:** O(n × k) — Full DP table stored.
This classic DP formulation uses `dp[i][j]` to represent the maximum profit achievable up to day `i` using at most `j` transactions. The `max_diff` variable tracks the best "buy point" considering previous transaction profits, enabling O(1) updates per cell.
- approach_name: Brute Force (Exponential)
is_optimal: false
code: |
def max_profit(k: int, prices: list[int]) -> int:
def backtrack(day: int, transactions: int, holding: bool) -> int:
# Base cases
if day >= len(prices) or transactions == 0:
return 0
# Option 1: Do nothing today
profit = backtrack(day + 1, transactions, holding)
if holding:
# Option 2: Sell today (complete a transaction)
profit = max(profit, prices[day] + backtrack(day + 1, transactions - 1, False))
else:
# Option 2: Buy today
profit = max(profit, -prices[day] + backtrack(day + 1, transactions, True))
return profit
return backtrack(0, k, False)
explanation: |
**Time Complexity:** O(2^n) — Exponential due to exploring all buy/sell combinations.
**Space Complexity:** O(n) — Recursion stack depth.
This recursive approach explores all possible decisions (buy, sell, or hold) at each day. While correct, it's far too slow for the given constraints. Included to show the problem structure before optimisation. Adding memoisation would make this equivalent to the DP solution.