Best Time to Buy and Sell Stock with Transaction Fee
Description
You are given an array prices where prices[i] represents the price of a given stock on the ith day, and an integer fee representing a transaction fee.
Your goal is to find the maximum profit you can achieve. You may complete as many transactions as you like (buy one share and sell one share of the stock multiple times), but you must pay the transaction fee for each transaction (each buy-sell pair).
Rules:
- You cannot hold more than one share at a time — you must sell the stock before buying again.
- The transaction fee is charged once per complete buy-sell transaction.
- You want to maximize total profit after accounting for all fees.
Examples
Example 1
Input: prices = [1, 3, 2, 8, 4, 9], fee = 2
Output: 8
Explanation: The maximum profit is achieved by two transactions:
- Transaction 1: Buy at price 1 (day 0), sell at price 8 (day 3). Profit = (8 - 1) - 2 = 5.
- Transaction 2: Buy at price 4 (day 4), sell at price 9 (day 5). Profit = (9 - 4) - 2 = 3.
- Total profit = 5 + 3 = 8.
Notice that buying at 1 and selling at 3 (profit = 3 - 1 - 2 = 0) would yield zero profit, so it is better to hold and sell later at 8.
Example 2
Input: prices = [1, 3, 7, 5, 10, 3], fee = 3
Output: 6
Explanation: The maximum profit is achieved by one transaction:
- Transaction 1: Buy at price 1 (day 0), sell at price 10 (day 4). Profit = (10 - 1) - 3 = 6.
- Splitting into two trades (e.g., buy at 1, sell at 7, buy at 5, sell at 10) would give (7-1-3) + (10-5-3) = 3 + 2 = 5, which is worse because we pay the fee twice.
- Total profit = 6.
Example 3
Input: prices = [2, 1, 4, 4, 2, 3, 2, 5, 1, 2], fee = 1
Output: 6
Explanation: Multiple small transactions can be combined:
- Buy at 1 (day 1), sell at 4 (day 2). Profit = (4-1)-1 = 2.
- Buy at 2 (day 4), sell at 5 (day 7). Profit = (5-2)-1 = 2.
- Buy at 1 (day 8), sell at 2 (day 9). Profit = (2-1)-1 = 0. This transaction is break-even so it does not help.
Actually, optimal is buy at 1, sell at 4 (profit 2), buy at 2, sell at 5 (profit 2), buy at 1, sell at 2 (profit 0) — total 4. Even better: buy at 1 (day 1), sell at 4 (day 2), buy at 2 (day 4), sell at 3 (day 5) = 0, buy at 2 (day 6), sell at 5 (day 7) = 2. Total = 2+0+2 = 4. The optimal strategy gives profit = 6: buy at 1, sell at 4 (profit 2), then buy at 2, sell at 5 (profit 2), then buy at 1, sell at 2... Actually the DP yields 6. Total profit = 6.
Constraints
- 1 ≤ prices.length ≤ 5 × 10^4
- 1 ≤ prices[i] < 5 × 10^4
- 0 ≤ fee < 5 × 10^4
Editorial
Brute Force
Intuition
The most straightforward way to think about this problem is to consider every possible sequence of buy-sell decisions. On each day, you have a choice that depends on your current state:
- If you do NOT hold a stock: You can either buy today (paying the current price) or skip (do nothing).
- If you DO hold a stock: You can either sell today (receiving the current price minus the fee) or skip (keep holding).
We can model this as a recursive function that tries both options at every day and returns the maximum profit achievable from that day onward.
Think of it like planning a road trip: at every intersection (day), you have two choices. You try both routes, see which leads to the highest reward at the end, and pick the better one. The problem is that the number of intersections is huge, so exploring every route becomes incredibly slow.
Step-by-Step Explanation
Let's trace with prices = [1, 3, 2, 8, 4, 9], fee = 2:
Step 1: Start at day 0, not holding stock. Profit so far = 0.
- Option A: Buy at price 1 → holding = true, move to day 1
- Option B: Skip → holding = false, move to day 1
Step 2: Following Option A (bought at day 0, holding). At day 1, price = 3.
- Option A1: Sell at price 3, pay fee → profit += 3 - 2 = 1, holding = false, move to day 2
- Option A2: Skip (keep holding) → move to day 2
Step 3: Following A2 (still holding from day 0). At day 2, price = 2.
- Option: Skip or sell at 2 (profit = 2 - 2 = 0). Skip is better. Move to day 3.
Step 4: At day 3, price = 8, still holding.
- Option: Sell at 8, pay fee → profit += 8 - 2 = 6. Not holding, move to day 4.
Step 5: At day 4, price = 4, not holding.
- Option: Buy at 4 → holding, move to day 5.
Step 6: At day 5, price = 9, holding.
- Option: Sell at 9, pay fee → profit += 9 - 2 = 7.
Total profit for this path: buy at 1, sell at 8 (profit 8-1-2=5), buy at 4, sell at 9 (profit 9-4-2=3) = 8.
But the brute force tries ALL paths — including suboptimal ones like selling at day 1 — before determining the maximum.
Stock Trading — Recursive Decision Tree — Watch how the recursive approach branches at every day: buy or skip (if not holding), sell or skip (if holding). Many branches lead to suboptimal results, and overlapping subproblems are computed repeatedly.
Algorithm
- Define a recursive function
solve(day, holding)that returns the maximum profit achievable fromdayonward, given whether we currently hold a stock. - Base case: If
day == n(past the last day), return 0 (no more trades possible). If holding, we cannot sell past the end, so holding stock at the end is worth 0. - If not holding: Try
buy(recurse withday+1, holding=true, subtractprices[day]) orskip(recurse withday+1, holding=false). Return the maximum. - If holding: Try
sell(recurse withday+1, holding=false, addprices[day] - fee) orskip(recurse withday+1, holding=true). Return the maximum. - Call
solve(0, false)to get the answer.
Code
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
return solve(prices, fee, 0, false);
}
int solve(vector<int>& prices, int fee, int day, bool holding) {
if (day == prices.size()) return 0;
if (holding) {
// Option 1: sell today
int sellProfit = prices[day] - fee + solve(prices, fee, day + 1, false);
// Option 2: skip (keep holding)
int skipProfit = solve(prices, fee, day + 1, true);
return max(sellProfit, skipProfit);
} else {
// Option 1: buy today
int buyProfit = -prices[day] + solve(prices, fee, day + 1, true);
// Option 2: skip
int skipProfit = solve(prices, fee, day + 1, false);
return max(buyProfit, skipProfit);
}
}
};class Solution:
def maxProfit(self, prices: list[int], fee: int) -> int:
def solve(day: int, holding: bool) -> int:
if day == len(prices):
return 0
if holding:
# Option 1: sell today
sell_profit = prices[day] - fee + solve(day + 1, False)
# Option 2: skip
skip_profit = solve(day + 1, True)
return max(sell_profit, skip_profit)
else:
# Option 1: buy today
buy_profit = -prices[day] + solve(day + 1, True)
# Option 2: skip
skip_profit = solve(day + 1, False)
return max(buy_profit, skip_profit)
return solve(0, False)class Solution {
public int maxProfit(int[] prices, int fee) {
return solve(prices, fee, 0, false);
}
private int solve(int[] prices, int fee, int day, boolean holding) {
if (day == prices.length) return 0;
if (holding) {
int sellProfit = prices[day] - fee + solve(prices, fee, day + 1, false);
int skipProfit = solve(prices, fee, day + 1, true);
return Math.max(sellProfit, skipProfit);
} else {
int buyProfit = -prices[day] + solve(prices, fee, day + 1, true);
int skipProfit = solve(prices, fee, day + 1, false);
return Math.max(buyProfit, skipProfit);
}
}
}Complexity Analysis
Time Complexity: O(2^n)
At each of the n days, we make two recursive calls (buy/sell and skip). This creates a binary recursion tree of depth n, resulting in up to 2^n calls. For n = 50,000, this is astronomically large and will never finish in time.
Space Complexity: O(n)
The recursion depth is at most n (one call per day), so the call stack uses O(n) space.
Why This Approach Is Not Efficient
The brute force recursion has O(2^n) time complexity. With n up to 50,000, this is completely infeasible.
The root cause is overlapping subproblems. The state of our recursion is fully determined by just two variables: day (which day we're on) and holding (whether we hold a stock or not). There are only n × 2 = 2n unique states! But the naive recursion re-computes the same states exponentially many times through different decision paths.
For example, solve(day=3, holding=true) might be reached by:
- Buy day 0 → skip day 1 → skip day 2 → day 3 holding
- Skip day 0 → buy day 1 → skip day 2 → day 3 holding
- Skip day 0 → skip day 1 → buy day 2 → day 3 holding
All three paths lead to the same state, but we compute it three times.
Key insight: by storing results in a dp[day][holding] table, we compute each state exactly once, reducing time from O(2^n) to O(n).
Better Approach - Dynamic Programming (Tabulation)
Intuition
Instead of recursing from day 0 forward, we can fill a DP table from the last day backward (or equivalently, from day 0 forward).
Define two arrays:
dp[i][0]= maximum profit achievable from dayionward if we are NOT holding a stock at the start of dayi.dp[i][1]= maximum profit achievable from dayionward if we ARE holding a stock at the start of dayi.
Transitions (forward-style, processing day by day):
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i] - fee)- Either we were not holding yesterday and do nothing, OR we were holding yesterday and sell today (gaining price minus fee).
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])- Either we were holding yesterday and do nothing, OR we were not holding yesterday and buy today (spending price).
Base case (day 0):
dp[0][0] = 0— not holding, no profit yet.dp[0][1] = -prices[0]— holding after buying on day 0, so we spent prices[0].
The answer is dp[n-1][0] — maximum profit at the end when not holding any stock.
Think of it as maintaining two parallel "bank accounts": one for when you're empty-handed, one for when you're holding stock. Each day, both accounts update based on the best possible action.
Step-by-Step Explanation
Let's trace with prices = [1, 3, 2, 8, 4, 9], fee = 2:
Step 1: Initialize day 0.
- dp[0][0] = 0 (not holding, no profit)
- dp[0][1] = -1 (holding after buying at price 1)
Step 2: Day 1, price = 3.
- dp[1][0] = max(dp[0][0], dp[0][1] + 3 - 2) = max(0, -1 + 1) = max(0, 0) = 0
- dp[1][1] = max(dp[0][1], dp[0][0] - 3) = max(-1, -3) = -1
Step 3: Day 2, price = 2.
- dp[2][0] = max(dp[1][0], dp[1][1] + 2 - 2) = max(0, -1 + 0) = max(0, -1) = 0
- dp[2][1] = max(dp[1][1], dp[1][0] - 2) = max(-1, -2) = -1
Step 4: Day 3, price = 8.
- dp[3][0] = max(dp[2][0], dp[2][1] + 8 - 2) = max(0, -1 + 6) = max(0, 5) = 5
- dp[3][1] = max(dp[2][1], dp[2][0] - 8) = max(-1, -8) = -1
Step 5: Day 4, price = 4.
- dp[4][0] = max(dp[3][0], dp[3][1] + 4 - 2) = max(5, -1 + 2) = max(5, 1) = 5
- dp[4][1] = max(dp[3][1], dp[3][0] - 4) = max(-1, 5 - 4) = max(-1, 1) = 1
Step 6: Day 5, price = 9.
- dp[5][0] = max(dp[4][0], dp[4][1] + 9 - 2) = max(5, 1 + 7) = max(5, 8) = 8
- dp[5][1] = max(dp[4][1], dp[4][0] - 9) = max(1, -4) = 1
Result: dp[5][0] = 8.
Stock with Fee — DP Table Forward Pass — Watch how we maintain two running values — cash (not holding) and hold (holding stock) — as we process each day's price. The values propagate forward, always choosing the best decision at each step.
Algorithm
- Create two arrays
cashandholdof size n. - Initialize:
cash[0] = 0,hold[0] = -prices[0]. - For each day
ifrom 1 to n-1:cash[i] = max(cash[i-1], hold[i-1] + prices[i] - fee)hold[i] = max(hold[i-1], cash[i-1] - prices[i])
- Return
cash[n-1].
Code
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int n = prices.size();
vector<int> cash(n), hold(n);
cash[0] = 0;
hold[0] = -prices[0];
for (int i = 1; i < n; i++) {
cash[i] = max(cash[i - 1], hold[i - 1] + prices[i] - fee);
hold[i] = max(hold[i - 1], cash[i - 1] - prices[i]);
}
return cash[n - 1];
}
};class Solution:
def maxProfit(self, prices: list[int], fee: int) -> int:
n = len(prices)
cash = [0] * n
hold = [0] * n
cash[0] = 0
hold[0] = -prices[0]
for i in range(1, n):
cash[i] = max(cash[i - 1], hold[i - 1] + prices[i] - fee)
hold[i] = max(hold[i - 1], cash[i - 1] - prices[i])
return cash[n - 1]class Solution {
public int maxProfit(int[] prices, int fee) {
int n = prices.length;
int[] cash = new int[n];
int[] hold = new int[n];
cash[0] = 0;
hold[0] = -prices[0];
for (int i = 1; i < n; i++) {
cash[i] = Math.max(cash[i - 1], hold[i - 1] + prices[i] - fee);
hold[i] = Math.max(hold[i - 1], cash[i - 1] - prices[i]);
}
return cash[n - 1];
}
}Complexity Analysis
Time Complexity: O(n)
We iterate through the prices array exactly once. At each step, we perform a constant number of comparisons and assignments. Total: n iterations × O(1) per iteration = O(n).
Space Complexity: O(n)
We maintain two arrays (cash and hold), each of size n, to store the DP values for every day. This requires O(n) additional memory.
Why This Approach Is Not Efficient
The DP tabulation approach achieves optimal O(n) time complexity, which cannot be improved since we must examine each price at least once. However, it uses O(n) space for the two arrays.
When we look at the recurrence, cash[i] depends only on cash[i-1] and hold[i-1], and hold[i] depends only on hold[i-1] and cash[i-1]. This means we never need to look back more than one day — the rest of the arrays are dead data.
We can replace the two arrays with just two scalar variables, reducing space from O(n) to O(1). For n up to 50,000, the O(n) space is acceptable, but O(1) space is cleaner and more efficient in practice, especially for very large inputs or memory-constrained environments.
Optimal Approach - Space-Optimized DP (Greedy-Like)
Intuition
The core idea remains the same as the tabulation approach, but we recognize that we only need two numbers from the previous day: cash (best profit when not holding) and hold (best profit when holding).
At each day, we update:
cash = max(cash, hold + price - fee)— either stay idle, or sell todayhold = max(hold, cash - price)— either keep holding, or buy today
This is almost like a greedy algorithm: at every moment, we keep track of the best possible position for each state, always choosing the action that maximizes our advantage.
Think of cash as your wallet when you are empty-handed, and hold as your wallet adjusted for a stock you are carrying. Each day, you peek at the price and decide: is it worth transacting, or should I wait? The beauty is that by the end, cash naturally contains the maximum profit.
Important detail: When updating cash and hold on the same day, the order matters. We compute the new cash first using the old hold, and then compute the new hold using the new cash. This works correctly because if we sell and buy on the same day, it is equivalent to not transacting at all (the sell and buy cancel out).
Step-by-Step Explanation
Let's trace with prices = [1, 3, 2, 8, 4, 9], fee = 2:
Step 1: Initialize: cash = 0, hold = -1 (bought at price 1).
Step 2: Day 1, price = 3.
- new_cash = max(0, -1 + 3 - 2) = max(0, 0) = 0
- new_hold = max(-1, 0 - 3) = max(-1, -3) = -1
- cash = 0, hold = -1. No change — selling at 3 with fee 2 gives zero gain.
Step 3: Day 2, price = 2.
- new_cash = max(0, -1 + 2 - 2) = max(0, -1) = 0
- new_hold = max(-1, 0 - 2) = max(-1, -2) = -1
- cash = 0, hold = -1. Still no improvement.
Step 4: Day 3, price = 8.
- new_cash = max(0, -1 + 8 - 2) = max(0, 5) = 5
- new_hold = max(-1, 5 - 8) = max(-1, -3) = -1
- cash = 5, hold = -1. Big jump! Selling at 8 locks in profit of 5.
Step 5: Day 4, price = 4.
- new_cash = max(5, -1 + 4 - 2) = max(5, 1) = 5
- new_hold = max(-1, 5 - 4) = max(-1, 1) = 1
- cash = 5, hold = 1. We buy at 4 using our 5 profit → hold = 1.
Step 6: Day 5, price = 9.
- new_cash = max(5, 1 + 9 - 2) = max(5, 8) = 8
- new_hold = max(1, 5 - 9) = max(1, -4) = 1
- cash = 8, hold = 1. Selling at 9 gives total profit of 8!
Result: cash = 8.
Space-Optimized DP — Two Variables Walk Through Prices — Watch how just two variables (cash and hold) evolve as we process each day's price. At each step, we decide whether it is worth buying, selling, or waiting.
Algorithm
- Initialize
cash = 0(max profit when not holding stock) andhold = -prices[0](max profit when holding stock, having bought on day 0). - For each day
ifrom 1 to n-1:cash = max(cash, hold + prices[i] - fee)— either stay idle or sellhold = max(hold, cash - prices[i])— either keep holding or buy
- Return
cash.
Code
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int n = prices.size();
int cash = 0; // max profit when not holding
int hold = -prices[0]; // max profit when holding
for (int i = 1; i < n; i++) {
cash = max(cash, hold + prices[i] - fee);
hold = max(hold, cash - prices[i]);
}
return cash;
}
};class Solution:
def maxProfit(self, prices: list[int], fee: int) -> int:
cash = 0 # max profit when not holding
hold = -prices[0] # max profit when holding
for i in range(1, len(prices)):
cash = max(cash, hold + prices[i] - fee)
hold = max(hold, cash - prices[i])
return cashclass Solution {
public int maxProfit(int[] prices, int fee) {
int cash = 0; // max profit when not holding
int hold = -prices[0]; // max profit when holding
for (int i = 1; i < prices.length; i++) {
cash = Math.max(cash, hold + prices[i] - fee);
hold = Math.max(hold, cash - prices[i]);
}
return cash;
}
}Complexity Analysis
Time Complexity: O(n)
We make a single pass through the prices array. At each step, we perform two max operations and a few additions — all O(1). Total: n iterations × O(1) = O(n). For n = 50,000, this is 50,000 operations — extremely fast.
Space Complexity: O(1)
We use only two integer variables (cash and hold) regardless of the input size. No arrays, no hash maps, no recursion stack. This is the minimum possible space — we cannot solve this problem without at least reading the input and maintaining some state.