Problem Overview
LeetCode 714 Best Time to Buy and Sell Stock with Transaction Fee gives you an integer array prices where prices[i] is the price of a given stock on day i, and an integer fee representing a transaction fee per trade. You may complete as many transactions as you like — buy and sell repeatedly — but you must pay the fee each time you sell. You may not hold more than one share at a time. Maximize total profit.
The fee fundamentally changes the strategy compared to LeetCode 122 (Stock II, unlimited transactions, no fee). Without a fee, you greedily take every upward movement. With a fee, small upward movements may not be worth taking. A trade is only worthwhile if the selling price minus the buying price exceeds the fee. The DP state machine handles this automatically without any explicit greedy reasoning.
The problem sits at the intersection of greedy and dynamic programming thinking. While a pure greedy approach becomes complicated with fees, the DP state machine elegantly encodes the optimal decision at each day by tracking just two values: how much money you have when you are not holding stock, and how much effective cash you have when you are holding stock.
- Given: integer array prices (prices[i] = stock price on day i) and integer fee
- You may buy and sell unlimited times, but only one share at a time
- Pay fee on each sell transaction
- You cannot hold more than one share simultaneously
- Goal: maximize total profit after all fees
- Constraints: 1 <= prices.length <= 5×10^4, 1 <= prices[i] <= 10^4, 0 <= fee <= 10^4
DP State Machine
The state machine has exactly two states: cash (you are not holding any stock) and hold (you are holding one share). You initialize cash = 0 (you start with zero profit and no stock) and hold = -prices[0] (on day 0, if you buy, your effective cash position is -prices[0]). Then for each subsequent day, you update both states simultaneously.
The two recurrences are: cash = max(cash, hold + prices[i] - fee) and hold = max(hold, cash - prices[i]). The first equation says: on day i, you either do nothing (stay in cash state) or you sell at prices[i] and pay the fee (transition from hold to cash). The second equation says: on day i, you either do nothing (stay in hold state) or you buy at prices[i] (transition from cash to hold).
After processing all days, the answer is cash — the maximum profit when you are not holding any stock. You never want to end holding stock because it means you paid for a share without recouping its value. The state machine requires only two variables and one pass through the prices array: O(n) time and O(1) space.
State Machine Framework Solves ALL Buy-Sell-Stock Variants
The two-state cash/hold machine is the universal skeleton for all six buy-sell-stock problems. Stock I: hold can only be set once. Stock II: no fee, same transitions. Stock III: add buy1/sell1/buy2/sell2 states. Stock IV: generalize to k buy/sell state pairs. Stock with Cooldown: cash transitions to a cooldown state before returning to cash. Stock with Fee: subtract fee on sell. Change the transitions — keep the same skeleton.
State Transitions
There are four possible actions on any given day, forming four transitions in the state machine. Cash-to-cash means you do nothing while not holding stock — your cash balance stays the same. Hold-to-cash means you sell: you receive prices[i] but pay the fee, so cash increases by prices[i] - fee. Cash-to-hold means you buy: you pay prices[i] from your effective cash, so hold = cash - prices[i]. Hold-to-hold means you do nothing while holding stock — your hold position stays the same.
The key design decision is where to subtract the fee. You can subtract it on sell (hold + prices[i] - fee) or equivalently on buy (cash - prices[i] - fee). The math is identical — only one fee per round trip regardless. Subtracting on sell is more intuitive because you pay when you take profit. Subtracting on buy also works but mentally feels like you pay upfront, which can confuse interview interviewers.
The simultaneous update is important: both cash and hold use the values from the previous day. If you update cash first and then use the updated cash to compute hold, you would allow buying and selling on the same day, which is not permitted. In practice, because both new values are computed from the old values before any overwrite, a simple sequential assignment works correctly in most implementations.
- 1cash-to-cash: do nothing (not holding) — cash stays the same
- 2hold-to-cash: sell at prices[i] and pay fee — cash = hold + prices[i] - fee
- 3cash-to-hold: buy at prices[i] — hold = cash - prices[i]
- 4hold-to-hold: do nothing (holding) — hold stays the same
- 5Each day: cash = max(cash, hold + prices[i] - fee)
- 6Each day: hold = max(hold, cash - prices[i]) [use OLD cash value]
- 7Fee is subtracted on sell — do NOT subtract on both buy and sell
Connection to Stock II
LeetCode 122 Stock II (unlimited transactions, no fee) is the direct predecessor to this problem. Without the fee, the recurrences become cash = max(cash, hold + prices[i]) and hold = max(hold, cash - prices[i]). This is equivalent to the greedy approach of summing every positive daily difference: any upward movement is worth capturing.
The fee changes the threshold for profitable trades. With fee f, a sell at price p from a buy at price b is profitable only if p - b > f. Small fluctuations below the fee are ignored by the DP automatically — the hold-to-cash transition will not update cash unless hold + prices[i] - fee exceeds the current cash. Trades that do not clear the fee threshold simply leave both states unchanged.
This automatic threshold behavior is what makes the DP state machine superior to a greedy approach for the fee variant. A greedy approach would need to reason about future prices to decide whether to sell now or hold for a bigger gain later. The DP does this implicitly by considering both options at every step and keeping the maximum.
Fee Prevents Churning — The DP Handles This Automatically
Without a fee you would buy and sell on every upward tick, even if the gain is $0.01. With a fee, a trade costs f dollars regardless of size. The DP automatically avoids trades where hold + prices[i] - fee does not exceed current cash — meaning it will not trigger a sell unless the gain clears the fee. You never need to add explicit logic to skip small trades; the max() in the transition handles it. This is the elegance of the state machine formulation.
Walk-Through Example
Consider prices = [1, 3, 2, 8, 4, 9] and fee = 2. Initialize cash = 0, hold = -1 (bought on day 0 at price 1). Day 1 (price=3): hold+3-2=0 which equals cash=0, so cash stays 0; cash-3=-3 which is less than hold=-1, so hold stays -1. Day 2 (price=2): hold+2-2=-1 < cash=0, no sell; cash-2=-2 < hold=-1, no buy. Day 3 (price=8): hold+8-2=5 > cash=0, so cash=5; hold=max(-1, 5-8)=max(-1,-3)=-1.
Continuing from day 3 (cash=5, hold=-1). Day 4 (price=4): hold+4-2=1 < cash=5, no sell; cash-4=1 > hold=-1, so hold=1 (we bought at 4, having 5 cash, effective position 5-4=1). Day 5 (price=9): hold+9-2=8 > cash=5, so cash=8; hold=max(1, 5-9)=max(1,-4)=1. Final answer: cash = 8.
The optimal trades are buy at 1 and sell at 8 (profit = 8-1-2 = 5) and buy at 4 and sell at 9 (profit = 9-4-2 = 3), for a total profit of 8. We correctly skipped selling at day 1 (price=3, profit 3-1-2=0) and buying back at day 2 (price=2) because the DP held the position to capture the larger move to 8. The state machine discovered this optimal hold-through automatically.
- 1Initialize: cash=0, hold=-prices[0]=-1
- 2Day 1 (price=3): cash=max(0, -1+3-2)=max(0,0)=0; hold=max(-1, 0-3)=max(-1,-3)=-1
- 3Day 2 (price=2): cash=max(0, -1+2-2)=max(0,-1)=0; hold=max(-1, 0-2)=max(-1,-2)=-1
- 4Day 3 (price=8): cash=max(0, -1+8-2)=max(0,5)=5; hold=max(-1, 5-8)=max(-1,-3)=-1
- 5Day 4 (price=4): cash=max(5, -1+4-2)=max(5,1)=5; hold=max(-1, 5-4)=max(-1,1)=1
- 6Day 5 (price=9): cash=max(5, 1+9-2)=max(5,8)=8; hold=max(1, 5-9)=max(1,-4)=1
- 7Answer: cash = 8 (buy@1 sell@8 profit=5, buy@4 sell@9 profit=3)
Code Walkthrough — Python and Java
Python implementation: def maxProfit(prices, fee): cash, hold = 0, -prices[0]; for price in prices[1:]: cash, hold = max(cash, hold+price-fee), max(hold, cash-price); return cash. The simultaneous assignment (cash, hold = ..., ...) on a single line ensures both new values are computed from the old values before either is overwritten. This is idiomatic Python for the state machine. O(n) time, O(1) space.
Java implementation: public int maxProfit(int[] prices, int fee) { int cash = 0, hold = -prices[0]; for (int i = 1; i < prices.length; i++) { int newCash = Math.max(cash, hold + prices[i] - fee); int newHold = Math.max(hold, cash - prices[i]); cash = newCash; hold = newHold; } return cash; }. In Java, the explicit temp variables newCash and newHold are necessary to avoid overwriting cash before computing the new hold. This is the same simultaneous update as Python, just written more explicitly.
Both implementations loop once through prices with constant work per step — O(n) time overall and O(1) space since only two integers are maintained. There is no DP array needed: the state machine replaces the full array with two running maximums. In an interview, sketch the state diagram first, write the two recurrences, then code the loop. Mention the simultaneous update requirement and where the fee is subtracted.
Subtract Fee on Sell OR Buy — Never Both
You can subtract the fee when transitioning hold-to-cash (on sell: hold + prices[i] - fee) or when transitioning cash-to-hold (on buy: cash - prices[i] - fee). Both are correct and produce the same result — one fee per round trip either way. The mistake is subtracting on both sell and buy, which doubles the fee and produces profit that is too low. Pick one convention and be consistent throughout both recurrences. Subtracting on sell is standard because it reads naturally: you pay the fee when you take the profit.