249 lines
12 KiB
YAML
249 lines
12 KiB
YAML
title: Best Time to Buy and Sell Stock III
|
|
slug: best-time-to-buy-and-sell-stock-iii
|
|
difficulty: hard
|
|
leetcode_id: 123
|
|
leetcode_url: https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/
|
|
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: [3, 3, 5, 0, 0, 3, 1, 4] }
|
|
expected: 6
|
|
- input: { prices: [1, 2, 3, 4, 5] }
|
|
expected: 4
|
|
- input: { prices: [7, 6, 4, 3, 1] }
|
|
expected: 0
|
|
hidden:
|
|
- input: { prices: [1] }
|
|
expected: 0
|
|
- input: { prices: [1, 2] }
|
|
expected: 1
|
|
- input: { prices: [2, 1, 2, 0, 1] }
|
|
expected: 2
|
|
- input: { prices: [1, 2, 4, 2, 5, 7, 2, 4, 9, 0] }
|
|
expected: 13
|
|
- input: { prices: [6, 1, 3, 2, 4, 7] }
|
|
expected: 7
|
|
- input: { prices: [3, 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 **at most two transactions**.
|
|
|
|
**Note:** You may not engage in multiple transactions simultaneously (i.e., you must sell the stock before you buy again).
|
|
|
|
constraints: |
|
|
- `1 <= prices.length <= 10^5`
|
|
- `0 <= prices[i] <= 10^5`
|
|
|
|
examples:
|
|
- input: "prices = [3,3,5,0,0,3,1,4]"
|
|
output: "6"
|
|
explanation: "Buy on day 4 (price = 0) and sell on day 6 (price = 3), profit = 3-0 = 3. Then buy on day 7 (price = 1) and sell on day 8 (price = 4), profit = 4-1 = 3. Total profit = 6."
|
|
- input: "prices = [1,2,3,4,5]"
|
|
output: "4"
|
|
explanation: "Buy on day 1 (price = 1) and sell on day 5 (price = 5), profit = 5-1 = 4. Even though we're allowed two transactions, one transaction achieves the maximum profit here."
|
|
- input: "prices = [7,6,4,3,1]"
|
|
output: "0"
|
|
explanation: "Prices only decrease, so no profitable transaction is possible. Return 0."
|
|
|
|
explanation:
|
|
intuition: |
|
|
This problem extends the classic "Best Time to Buy and Sell Stock" by allowing **at most two transactions** instead of one. The key insight is that we need to track the state of our trading at each point in time.
|
|
|
|
Think of it like this: at any moment, you're in one of several states:
|
|
- You haven't done anything yet
|
|
- You've bought your first stock (holding it)
|
|
- You've completed your first transaction (sold it)
|
|
- You've bought your second stock (holding it)
|
|
- You've completed both transactions
|
|
|
|
The brilliant insight is that we can track **four variables** representing the best outcomes at each state:
|
|
- `buy1`: The maximum money we can have after buying the first stock (this is negative since we spent money)
|
|
- `sell1`: The maximum profit after selling the first stock
|
|
- `buy2`: The maximum money we can have after buying the second stock (profit from first transaction minus the second purchase)
|
|
- `sell2`: The maximum profit after selling the second stock
|
|
|
|
As we iterate through prices, we update these states greedily. The magic is that `buy2` builds upon `sell1`, allowing the second transaction to benefit from the first.
|
|
|
|
approach: |
|
|
We solve this using a **State Machine DP Approach** with four variables:
|
|
|
|
**Step 1: Initialise the state variables**
|
|
|
|
- `buy1`: Set to negative infinity (or `-prices[0]`) — represents the cost of buying the first stock
|
|
- `sell1`: Set to `0` — no profit until we sell
|
|
- `buy2`: Set to negative infinity (or `-prices[0]`) — represents buying the second stock
|
|
- `sell2`: Set to `0` — no profit until we complete both transactions
|
|
|
|
|
|
|
|
**Step 2: Iterate through prices and update states**
|
|
|
|
For each price, we update our states in this order (from last to first to avoid using updated values):
|
|
|
|
- `buy1 = max(buy1, -price)` — Either keep previous buy1, or buy today
|
|
- `sell1 = max(sell1, buy1 + price)` — Either keep previous sell1, or sell today
|
|
- `buy2 = max(buy2, sell1 - price)` — Either keep previous buy2, or buy second stock today using profit from first transaction
|
|
- `sell2 = max(sell2, buy2 + price)` — Either keep previous sell2, or complete the second sale
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- Return `sell2` — this represents the maximum profit from at most two transactions
|
|
- Note: `sell2` can represent 0, 1, or 2 transactions (it captures the best case)
|
|
|
|
|
|
|
|
This approach works because each state depends only on the previous states, and we process them in the right order to ensure consistency.
|
|
|
|
common_pitfalls:
|
|
- title: Using O(n) Space with Two Passes
|
|
description: |
|
|
A common approach is to split the array into two parts and find the best single transaction for each part:
|
|
- `left[i]`: Max profit from day 0 to day i
|
|
- `right[i]`: Max profit from day i to day n-1
|
|
|
|
Then find `max(left[i] + right[i+1])`. While this works and is O(n) time, it uses O(n) space for the two arrays.
|
|
|
|
The four-variable approach achieves the same result with O(1) space by recognising that we only need the running best states.
|
|
wrong_approach: "Two arrays tracking left and right max profits"
|
|
correct_approach: "Four variables tracking states of transactions"
|
|
|
|
- title: Wrong Update Order
|
|
description: |
|
|
When updating the four variables, the order matters. If you update `buy1` first and then use its new value for `sell1`, you might use today's buy price for today's sell price in the same iteration.
|
|
|
|
However, in this specific formulation, updating from `buy1` to `sell2` in sequence actually works correctly because:
|
|
- `buy1` uses only the current price
|
|
- `sell1` uses `buy1` which may have just been updated, but this just means we could buy and sell on the same day (profit = 0), which is valid
|
|
- The same logic applies to `buy2` and `sell2`
|
|
|
|
The key insight is that this "same day" operation doesn't hurt us — it just represents not doing a transaction.
|
|
wrong_approach: "Incorrect state transition order causing invalid states"
|
|
correct_approach: "Process states in sequence: buy1 → sell1 → buy2 → sell2"
|
|
|
|
- title: Not Handling Zero or One Transaction Cases
|
|
description: |
|
|
You might think you need special handling when:
|
|
- No profitable transaction exists (return 0)
|
|
- Only one transaction is optimal
|
|
|
|
But the state machine handles this naturally:
|
|
- `sell2` starts at 0, so if no profit is possible, it remains 0
|
|
- If one transaction is best, `buy2` can equal `sell1 - price` where we immediately "rebuy" at the same price, making the second transaction have zero effect
|
|
wrong_approach: "Special case handling for different transaction counts"
|
|
correct_approach: "Let the state machine naturally handle all cases"
|
|
|
|
key_takeaways:
|
|
- "**State machine DP**: Complex transaction problems can be modeled as state transitions — identify the states and their transitions"
|
|
- "**Space optimisation**: When DP states only depend on previous iteration, you can reduce from O(n) arrays to O(1) variables"
|
|
- "**Generalisation**: This approach extends to k transactions by using 2k variables (or a 2D array for large k)"
|
|
- "**Foundation for harder variants**: The same state machine concept applies to problems with cooldowns, fees, or unlimited transactions"
|
|
|
|
time_complexity: "O(n). We traverse the prices array exactly once, performing constant-time operations at each step."
|
|
space_complexity: "O(1). We only use four variables (`buy1`, `sell1`, `buy2`, `sell2`) regardless of input size."
|
|
|
|
solutions:
|
|
- approach_name: State Machine DP
|
|
is_optimal: true
|
|
code: |
|
|
def max_profit(prices: list[int]) -> int:
|
|
# Initialise states for two transactions
|
|
# buy1: best outcome after first buy
|
|
# sell1: best profit after first sell
|
|
# buy2: best outcome after second buy (using profit from first)
|
|
# sell2: best profit after both transactions complete
|
|
buy1 = buy2 = float('-inf')
|
|
sell1 = sell2 = 0
|
|
|
|
for price in prices:
|
|
# Update states - order matters for clarity but works in sequence
|
|
buy1 = max(buy1, -price) # Buy first stock
|
|
sell1 = max(sell1, buy1 + price) # Sell first stock
|
|
buy2 = max(buy2, sell1 - price) # Buy second using first profit
|
|
sell2 = max(sell2, buy2 + price) # Sell second stock
|
|
|
|
return sell2
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Single pass through the array.
|
|
|
|
**Space Complexity:** O(1) — Only four variables used.
|
|
|
|
We track four states representing different stages of completing up to two transactions. At each price, we update what the best outcome would be if we took that action today. The final `sell2` contains the maximum profit achievable.
|
|
|
|
- approach_name: Two-Pass with Left/Right Arrays
|
|
is_optimal: false
|
|
code: |
|
|
def max_profit(prices: list[int]) -> int:
|
|
if not prices:
|
|
return 0
|
|
|
|
n = len(prices)
|
|
|
|
# left[i] = max profit from a single transaction in prices[0:i+1]
|
|
left = [0] * n
|
|
min_price = prices[0]
|
|
for i in range(1, n):
|
|
min_price = min(min_price, prices[i])
|
|
left[i] = max(left[i - 1], prices[i] - min_price)
|
|
|
|
# right[i] = max profit from a single transaction in prices[i:n]
|
|
right = [0] * n
|
|
max_price = prices[n - 1]
|
|
for i in range(n - 2, -1, -1):
|
|
max_price = max(max_price, prices[i])
|
|
right[i] = max(right[i + 1], max_price - prices[i])
|
|
|
|
# Find the best split point for two transactions
|
|
max_profit = 0
|
|
for i in range(n):
|
|
max_profit = max(max_profit, left[i] + right[i])
|
|
|
|
return max_profit
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Three passes through the array.
|
|
|
|
**Space Complexity:** O(n) — Two arrays of size n.
|
|
|
|
This approach splits the problem: find the best single transaction ending at or before day i (`left[i]`), and the best single transaction starting at or after day i (`right[i]`). The answer is the maximum sum of `left[i] + right[i]` for any split point. While correct, it uses more space than the state machine approach.
|
|
|
|
- approach_name: Generalised k-Transactions DP
|
|
is_optimal: false
|
|
code: |
|
|
def max_profit(prices: list[int]) -> int:
|
|
if not prices:
|
|
return 0
|
|
|
|
k = 2 # At most 2 transactions
|
|
n = len(prices)
|
|
|
|
# dp[t][0] = max profit with t transactions, not holding stock
|
|
# dp[t][1] = max profit with t transactions, holding stock
|
|
dp = [[0, float('-inf')] for _ in range(k + 1)]
|
|
|
|
for price in prices:
|
|
for t in range(1, k + 1):
|
|
# Not holding: either stay not holding, or sell today
|
|
dp[t][0] = max(dp[t][0], dp[t][1] + price)
|
|
# Holding: either stay holding, or buy today (using t-1 profit)
|
|
dp[t][1] = max(dp[t][1], dp[t - 1][0] - price)
|
|
|
|
return dp[k][0]
|
|
explanation: |
|
|
**Time Complexity:** O(n * k) — For each price, update k transaction states.
|
|
|
|
**Space Complexity:** O(k) — Array of size k for transaction states.
|
|
|
|
This generalised approach handles any number of transactions k. For k=2, it's equivalent to the four-variable solution but structured to scale. Each `dp[t]` tracks the best outcomes for exactly t transactions. This is useful when k is a variable input rather than fixed at 2.
|