A clear explanation of maximizing stock profit with at most two transactions using dynamic programming.
Problem Restatement
We are given an array prices, where prices[i] is the stock price on day i.
We may complete at most two transactions.
One transaction means:
buy once, then sell onceWe cannot hold more than one share at a time, so we must sell before buying again.
We need to return the maximum possible profit.
For example:
prices = [3, 3, 5, 0, 0, 3, 1, 4]One best strategy is:
buy at 0, sell at 3 -> profit 3
buy at 1, sell at 4 -> profit 3Total profit:
6So the answer is:
6Input and Output
| Item | Meaning |
|---|---|
| Input | List of stock prices |
| Output | Maximum profit with at most two transactions |
| Transaction | One buy followed by one sell |
| Limit | At most two transactions |
| Holding rule | Cannot hold more than one share at a time |
| No profit case | Return 0 |
The function shape is:
class Solution:
def maxProfit(self, prices: list[int]) -> int:
...Examples
For:
prices = [3, 3, 5, 0, 0, 3, 1, 4]A best plan is:
buy at 0
sell at 3
buy at 1
sell at 4The total profit is:
3 + 3 = 6So the answer is:
6For:
prices = [1, 2, 3, 4, 5]The best profit is made by buying at 1 and selling at 5:
5 - 1 = 4Even though two transactions are allowed, we do not need to use both.
The answer is:
4For:
prices = [7, 6, 4, 3, 1]No profitable transaction exists.
The answer is:
0First Thought: Split the Problem
With one transaction, we keep the lowest price seen so far.
With two transactions, the best answer can be seen as:
best profit from first transaction
+
best profit from second transactionBut the second transaction must happen after the first transaction is complete.
A direct way is to try every split day:
left side: best one-transaction profit before or on this day
right side: best one-transaction profit after this dayThen take the best sum.
That works in O(n), but we can compress the idea further.
Key Insight
Track four states while scanning prices:
| State | Meaning |
|---|---|
buy1 | Best balance after first buy |
sell1 | Best profit after first sell |
buy2 | Best balance after second buy |
sell2 | Best profit after second sell |
A buy reduces our balance.
A sell increases our balance.
For each price, update:
buy1 = max(buy1, -price)
sell1 = max(sell1, buy1 + price)
buy2 = max(buy2, sell1 - price)
sell2 = max(sell2, buy2 + price)The final answer is:
sell2This also covers using only one transaction, because sell2 can carry forward the best result without forcing a second useful trade.
Algorithm
Initialize:
buy1 = -prices[0]
sell1 = 0
buy2 = -prices[0]
sell2 = 0Then scan each price.
For every price:
- Update the best first buy.
- Update the best first sell.
- Update the best second buy after the first sell.
- Update the best second sell.
Return sell2.
Correctness
At every day, buy1 stores the best possible balance after buying once. Since buying at price p gives balance -p, the best first buy is the largest value among all -price values seen so far.
sell1 stores the best profit after completing one transaction. Selling today after the best first buy gives buy1 + price, so taking the maximum keeps the best one-transaction profit.
buy2 stores the best possible balance after completing one transaction and then buying again. If we buy the second stock today, the balance becomes:
sell1 - priceTaking the maximum keeps the best second-buy state.
sell2 stores the best profit after completing at most two transactions. Selling the second stock today gives:
buy2 + priceTaking the maximum keeps the best two-transaction profit.
These four states represent all valid progress stages of at most two transactions, in valid chronological order. Therefore, after all prices are processed, sell2 is the maximum profit possible with at most two transactions.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | We scan the prices once |
| Space | O(1) | We store four state variables |
Implementation
class Solution:
def maxProfit(self, prices: list[int]) -> int:
buy1 = -prices[0]
sell1 = 0
buy2 = -prices[0]
sell2 = 0
for price in prices:
buy1 = max(buy1, -price)
sell1 = max(sell1, buy1 + price)
buy2 = max(buy2, sell1 - price)
sell2 = max(sell2, buy2 + price)
return sell2Code Explanation
The first buy starts as buying on day 0:
buy1 = -prices[0]Before selling, profit is zero:
sell1 = 0The second buy can also start from the first price as a neutral initialization:
buy2 = -prices[0]The best profit after two sells starts at zero:
sell2 = 0For each price, update the first buy:
buy1 = max(buy1, -price)Then update the first sell:
sell1 = max(sell1, buy1 + price)Then update the second buy:
buy2 = max(buy2, sell1 - price)Then update the second sell:
sell2 = max(sell2, buy2 + price)Finally:
return sell2Testing
class Solution:
def maxProfit(self, prices: list[int]) -> int:
buy1 = -prices[0]
sell1 = 0
buy2 = -prices[0]
sell2 = 0
for price in prices:
buy1 = max(buy1, -price)
sell1 = max(sell1, buy1 + price)
buy2 = max(buy2, sell1 - price)
sell2 = max(sell2, buy2 + price)
return sell2
def run_tests():
s = Solution()
assert s.maxProfit([3, 3, 5, 0, 0, 3, 1, 4]) == 6
assert s.maxProfit([1, 2, 3, 4, 5]) == 4
assert s.maxProfit([7, 6, 4, 3, 1]) == 0
assert s.maxProfit([1]) == 0
assert s.maxProfit([2, 1, 4, 5, 2, 9, 7]) == 11
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
[3,3,5,0,0,3,1,4] | Standard two-transaction example |
[1,2,3,4,5] | One transaction is enough |
[7,6,4,3,1] | No profit possible |
| Single price | Cannot complete a transaction |
| Mixed prices | Confirms two separate profitable windows |