LeetCode #123 Hard

Best Time to Buy and Sell Stock III

Best Time to Buy and Sell Stock III: max profit using at most two non-overlapping buy-sell transactions.

Constraints
  • 1 <= prices.length <= 10⁵
  • 0 <= prices[i] <= 10⁵
dynamic-programmingarraystate-machine
Open on LeetCode ↗
02

Intuition

Best time to buy and sell stock iii caps the number of transactions at two, sitting between the single-transaction original and the unlimited version. That cap is what makes it Hard — the greedy no longer applies, and a transaction count must be carried. The direct approach tracks four states, each representing how far through the two transactions you are: - First buy, first sell, second buy, second sell — a share is bought, sold, bought again, and sold again, in that order. Each state is the best balance achievable at that stage. firstBuy is the balance after one purchase, so it is negative. firstSell is the balance after one completed transaction. secondBuy is the balance after buying again out of the first sale's proceeds. secondSell is the final answer. What makes this correct is that each state builds on the one before it: secondBuy = max(secondBuy, firstSell − price) funds the second purchase from the profit already banked. That chaining is what enforces the ordering — the second transaction cannot begin before the first has finished. Updating all four in a single pass, in order, works even though later states read values updated earlier in the same iteration. Allowing a same-day sell and rebuy is harmless, since it produces a net-zero pair that never beats the alternative. Initialise both buy states to negative infinity — or firstBuy to −prices[0] — so an unfilled state never looks profitable. An alternative splits the array at every index, computing the best profit before and after each split point in two passes. Same O(n), and a useful cross-check.

How to spot this pattern

Four states chained in sequence: buy1 → sell1 → buy2 → sell2. Each one feeds the next, and updating them in that order within a single loop means the second transaction's purchase already sees the first sale's profit — the chaining does the bookkeeping.

03

Approach

Try it first

Before reading on: price up what the direct approach costs here, then ask what a single cell should mean, and which earlier cells it needs. Aim for O(n) time and O(1) space.

1

Recognise why the cap matters

At most two transactions means the greedy from the unlimited version fails, and a transaction count must be carried through the scan. That is the whole step up in difficulty.

2

Define four ordered states

First buy, first sell, second buy, second sell. Each is the best balance achievable at that stage, and their order encodes the required sequence of trades.

3

Update the first transaction

firstBuy = max(firstBuy, -price) and firstSell = max(firstSell, firstBuy + price). These mirror the single-transaction problem exactly.

4

Chain the second onto the first

secondBuy = max(secondBuy, firstSell - price) funds the second purchase from banked profit. This chaining is what enforces the ordering between the two transactions.

5

Complete the second sale

secondSell = max(secondSell, secondBuy + price) holds the final answer. Because a second transaction is optional, this never falls below firstSell.

6

Initialise the buy states low

Set both buy states to negative infinity, or firstBuy to -prices[0]. Starting at 0 makes an unfilled state look profitable and inflates the result.

7

Know the two-pass alternative

Split at every index, computing the best profit before and after each split. Same O(n) time, and a useful cross-check when the four-state version misbehaves.

8

Cost of the scan

One pass updating four variables gives O(n) time and O(1) space, with no table required despite the transaction limit.

04

Solution & live demo

▶1class Solution:
▶2 def maxProfit(self, prices:
▶3 list[int]) -> int:
▶4 n = len(prices)
▶5 if n < 2:
▶6 return 0
▶7 buy1 = buy2 = -prices[0]
▶8 sell1 = sell2 = 0
▶9 for i in range(1, n):
▶10 buy1 = max(buy1, -prices[i])
▶11 sell1 = max(sell1, buy1 + prices[i])
▶12 buy2 = max(buy2, sell1 - prices[i])
▶13 sell2 = max(sell2, buy2 + prices[i])
▶14 return sell2
05

Common pitfalls

Splitting the array at every index

✗ Wrong
for i in range(n):
    best = max(best, maxOne(prices[:i]) + maxOne(prices[i:]))
✓ Right
buy2 = max(buy2, sell1 - prices[i])

That's O(n²), or O(n) with two precomputed arrays. The four chained states carry the same information in four scalars and one pass.

Updating the states in reverse order

✗ Wrong
sell2 = max(sell2, buy2 + prices[i])
buy2 = max(buy2, sell1 - prices[i])
...
✓ Right
buy1 = ...; sell1 = ...; buy2 = ...; sell2 = ...

Each state depends on the one before it on the same day — that's what allows buying and selling on the same index, which is a legal no-op. Reversing the order breaks the chain and undercounts.

Initialising buy2 to zero

✗ Wrong
buy2 = 0
✓ Right
buy2 = -prices[0]

buy2 holds profit-minus-cost after two purchases, which starts negative. Seeding at 0 pretends a share was acquired for free and inflates the answer.

06

Edge cases

Strictly decreasing prices

buy1, buy2 chase the lowest price but sell1, sell2 never exceed 0, so the answer correctly stays 0 with no forced losing trade.

Only one profitable dip-then-rise in the whole array

sell2 stays equal to sell1 throughout since a second trade never improves on it, correctly reducing to the single-transaction answer.

Fewer than 2 price points

With 0 or 1 prices there is no valid transaction; the loop from day 1 onward simply does not run and profit stays 0.

Two disjoint profitable windows

buy2 only ever draws funds from sell1, which already reflects a closed first trade, so the two captured gains cannot overlap in time.

07

Complexity

Time
O(n)
Space
O(1)
Four rolling scalars replace a k=2 transaction DP table.