A clear explanation of maximizing stock trading profit with unlimited transactions and a fixed transaction fee using dynamic programming.
Problem Restatement
We are given an array prices.
prices[i]is the price of a stock on day i.
We are also given an integer fee.
We may complete as many transactions as we want, but we cannot hold more than one share at the same time. That means we must sell the current stock before buying again.
Each completed transaction pays the transaction fee once.
Return the maximum profit we can achieve.
The official problem states that the fee is paid once for each buy-sell transaction, and multiple simultaneous transactions are not allowed.
Input and Output
| Item | Meaning |
|---|---|
| Input | prices, a list of stock prices |
| Input | fee, the transaction fee |
| Output | Maximum possible profit |
| Allowed transactions | As many as we want |
| Restriction | Must sell before buying again |
| Fee rule | Fee is paid once per completed transaction |
The function shape is:
class Solution:
def maxProfit(self, prices: list[int], fee: int) -> int:
...Examples
Example 1:
prices = [1, 3, 2, 8, 4, 9]
fee = 2One optimal strategy is:
| Action | Price | Profit Change |
|---|---|---|
| Buy | 1 | -1 |
| Sell | 8 | +8 - 2 |
| Buy | 4 | -4 |
| Sell | 9 | +9 - 2 |
Total profit:
(8 - 1 - 2) + (9 - 4 - 2) = 8Output:
8Example 2:
prices = [1, 3, 7, 5, 10, 3]
fee = 3Output:
6First Thought: Try Every Buy and Sell Choice
A brute force approach is to consider every possible action on every day.
On each day, we can:
- Do nothing.
- Buy, if we are not holding a stock.
- Sell, if we are holding a stock.
This naturally leads to recursion.
The problem is that many states repeat. For example, reaching day i while holding a stock can happen through many different earlier action sequences.
Without memoization, this becomes exponential.
Key Insight
At the end of each day, we only need two states.
| State | Meaning |
|---|---|
cash | Maximum profit after this day while holding no stock |
hold | Maximum profit after this day while holding one stock |
Every valid strategy must be in exactly one of these states.
For each price, we update these two values.
If we end the day with no stock, either:
- We already had no stock.
- We sell today.
If we end the day holding stock, either:
- We were already holding.
- We buy today.
We can charge the fee when selling.
Recurrence
Let price be today’s price.
To update cash:
cash = max(cash, hold + price - fee)This means:
| Choice | Profit |
|---|---|
| Do not sell | cash |
| Sell today | hold + price - fee |
To update hold:
hold = max(hold, old_cash - price)This means:
| Choice | Profit |
|---|---|
| Do not buy | hold |
| Buy today | old_cash - price |
We use old_cash because buying should use the no-stock profit from before today’s update.
Algorithm
- Set
cash = 0. - Set
hold = -prices[0]. - For each later price:
- Save
old_cash = cash. - Update
cashby either keeping cash or selling. - Update
holdby either keeping hold or buying.
- Save
- Return
cash.
The answer is cash because the best final result should not end with an unsold stock. Holding a stock means we paid for it but did not realize its value as profit.
Correctness
We prove that after each day, cash and hold represent the best possible profits for their states.
At the start, before any sale, the best no-stock profit is 0. If we buy on the first day, the best holding profit is -prices[0]. So the initialization is correct.
Assume the values are correct before processing a day with price price.
To end the day with no stock, there are only two possibilities. Either we already had no stock and do nothing, giving profit cash, or we sell the stock we were holding, giving profit hold + price - fee. Taking the maximum gives the best no-stock profit.
To end the day holding stock, there are also only two possibilities. Either we were already holding and do nothing, giving profit hold, or we buy today using a previous no-stock state, giving profit old_cash - price. Taking the maximum gives the best holding profit.
These transitions cover all legal actions and exclude illegal simultaneous transactions because buying is only allowed from the no-stock state and selling is only allowed from the holding state.
Therefore the states remain correct after every day. At the end, the maximum realized profit is cash, so returning it is correct.
Complexity
Let n be the number of days.
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | We process each price once |
| Space | O(1) | We keep only two DP states |
Implementation
class Solution:
def maxProfit(self, prices: list[int], fee: int) -> int:
cash = 0
hold = -prices[0]
for price in prices[1:]:
old_cash = cash
cash = max(cash, hold + price - fee)
hold = max(hold, old_cash - price)
return cashCode Explanation
The no-stock state starts at zero profit:
cash = 0The holding state starts as if we bought on the first day:
hold = -prices[0]Before updating cash, save its old value:
old_cash = cashThen decide whether to sell today:
cash = max(cash, hold + price - fee)Then decide whether to buy today:
hold = max(hold, old_cash - price)Finally, return the best profit while holding no stock:
return cashAlternative Fee Placement
We can also charge the fee when buying instead of selling.
class Solution:
def maxProfit(self, prices: list[int], fee: int) -> int:
cash = 0
hold = -prices[0] - fee
for price in prices[1:]:
old_cash = cash
cash = max(cash, hold + price)
hold = max(hold, old_cash - price - fee)
return cashBoth versions are correct as long as the fee is charged exactly once per completed transaction.
Example Walkthrough
Use:
prices = [1, 3, 2, 8, 4, 9]
fee = 2Initialize:
cash = 0
hold = -1Process price 3:
cash = max(0, -1 + 3 - 2) = 0
hold = max(-1, 0 - 3) = -1Process price 2:
cash = max(0, -1 + 2 - 2) = 0
hold = max(-1, 0 - 2) = -1Process price 8:
cash = max(0, -1 + 8 - 2) = 5
hold = max(-1, 0 - 8) = -1Process price 4:
cash = max(5, -1 + 4 - 2) = 5
hold = max(-1, 5 - 4) = 1The hold = 1 state means we have already made profit 5, then bought again at price 4, leaving net profit 1.
Process price 9:
cash = max(5, 1 + 9 - 2) = 8
hold = max(1, 5 - 9) = 1Final answer:
8Testing
def test_max_profit_with_fee():
s = Solution()
assert s.maxProfit([1, 3, 2, 8, 4, 9], 2) == 8
assert s.maxProfit([1, 3, 7, 5, 10, 3], 3) == 6
assert s.maxProfit([1], 0) == 0
assert s.maxProfit([5, 4, 3, 2, 1], 1) == 0
assert s.maxProfit([1, 2, 3, 4, 5], 0) == 4
assert s.maxProfit([1, 4, 6, 2, 8, 3, 10, 14], 3) == 13
print("all tests passed")
test_max_profit_with_fee()Test coverage:
| Test | Why |
|---|---|
| Standard example | Confirms multiple transactions with fee |
| Second example | Confirms fee can merge nearby trades |
| Single day | Confirms no possible sale |
| Decreasing prices | Confirms no losing trade is forced |
| Zero fee | Reduces to unlimited stock trading |
| Multiple profitable intervals | Confirms repeated state transitions |