201 lines
9.4 KiB
YAML
201 lines
9.4 KiB
YAML
title: Best Time to Buy and Sell Stock with Transaction Fee
|
|
slug: best-time-to-buy-and-sell-stock-with-transaction-fee
|
|
difficulty: medium
|
|
leetcode_id: 714
|
|
leetcode_url: https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/
|
|
categories:
|
|
- arrays
|
|
- dynamic-programming
|
|
patterns:
|
|
- dynamic-programming
|
|
- greedy
|
|
|
|
function_signature: "def max_profit(prices: list[int], fee: int) -> int:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { prices: [1, 3, 2, 8, 4, 9], fee: 2 }
|
|
expected: 8
|
|
- input: { prices: [1, 3, 7, 5, 10, 3], fee: 3 }
|
|
expected: 6
|
|
hidden:
|
|
- input: { prices: [1], fee: 1 }
|
|
expected: 0
|
|
- input: { prices: [1, 2], fee: 1 }
|
|
expected: 0
|
|
- input: { prices: [1, 3], fee: 1 }
|
|
expected: 1
|
|
- input: { prices: [1, 5, 2, 8], fee: 2 }
|
|
expected: 6
|
|
- input: { prices: [2, 1, 4, 4, 2, 3, 2, 5, 1, 2], fee: 1 }
|
|
expected: 6
|
|
- input: { prices: [4, 5, 2, 4, 3, 3, 1, 2, 5, 4], fee: 1 }
|
|
expected: 4
|
|
|
|
description: |
|
|
You are given an array `prices` where `prices[i]` is the price of a given stock on the i<sup>th</sup> day, and an integer `fee` representing a transaction fee.
|
|
|
|
Find the maximum profit you can achieve. You may complete as many transactions as you like, but you need to pay the transaction fee for each transaction.
|
|
|
|
**Note:**
|
|
|
|
- You may not engage in multiple transactions simultaneously (i.e., you must sell the stock before you buy again).
|
|
- The transaction fee is only charged once for each stock purchase and sale.
|
|
|
|
constraints: |
|
|
- `1 <= prices.length <= 5 * 10^4`
|
|
- `1 <= prices[i] < 5 * 10^4`
|
|
- `0 <= fee < 5 * 10^4`
|
|
|
|
examples:
|
|
- input: "prices = [1,3,2,8,4,9], fee = 2"
|
|
output: "8"
|
|
explanation: "The maximum profit can be achieved by: Buying at prices[0] = 1, selling at prices[3] = 8, buying at prices[4] = 4, selling at prices[5] = 9. The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8."
|
|
- input: "prices = [1,3,7,5,10,3], fee = 3"
|
|
output: "6"
|
|
explanation: "Buy at prices[0] = 1, sell at prices[4] = 10. Profit = (10 - 1) - 3 = 6."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Think of this as an extension of the classic "Best Time to Buy and Sell Stock II" problem, where you could make unlimited transactions for free. The fee introduces a **threshold for action** — a small profit that doesn't cover the fee isn't worth the transaction.
|
|
|
|
Imagine you're a trader who has to pay a broker fee every time you complete a buy-sell cycle. This fee changes the decision calculus: you might skip a small profitable opportunity because the fee would eat into (or exceed) your gains.
|
|
|
|
The key insight is to track **two states** at each day:
|
|
|
|
- **Holding state**: The maximum profit if you currently hold a stock (either you bought today, or you're continuing to hold from before)
|
|
- **Not holding state**: The maximum profit if you don't hold a stock (either you sold today, or you were already not holding)
|
|
|
|
At each day, you transition between these states. The fee is paid when you sell (completing the transaction), which affects whether selling is worthwhile.
|
|
|
|
This state-machine thinking lets us solve the problem in a single pass, updating our two states based on the optimal choice at each step.
|
|
|
|
approach: |
|
|
We use a **State Machine DP** approach with two variables tracking our optimal profit in each state:
|
|
|
|
**Step 1: Initialise two state variables**
|
|
|
|
- `hold`: The maximum profit when holding a stock. Initially `-prices[0]` because buying on day 0 costs us that amount
|
|
- `cash`: The maximum profit when not holding a stock. Initially `0` because we start with no stock and no profit
|
|
|
|
|
|
|
|
**Step 2: Iterate through prices starting from day 1**
|
|
|
|
For each day, we have two choices depending on our current state:
|
|
|
|
- **If we want to end up holding**: Either we already held (keep `hold`), or we buy today (`cash - prices[i]`). Take the maximum.
|
|
- **If we want to end up with cash**: Either we already had cash (keep `cash`), or we sell today (`hold + prices[i] - fee`). Take the maximum.
|
|
|
|
|
|
|
|
**Step 3: Update states simultaneously**
|
|
|
|
Use temporary variables or update in the right order to avoid using updated values in the same iteration:
|
|
|
|
- `new_hold = max(hold, cash - prices[i])`
|
|
- `new_cash = max(cash, hold + prices[i] - fee)`
|
|
|
|
|
|
|
|
**Step 4: Return the cash state**
|
|
|
|
After processing all days, `cash` contains the maximum profit (we want to end without holding stock).
|
|
|
|
common_pitfalls:
|
|
- title: Greedy Without Considering the Fee
|
|
description: |
|
|
In the version without fees, you can greedily take every small upward movement: if `prices[i] > prices[i-1]`, add the difference.
|
|
|
|
With fees, this greedy approach fails. For example, with `prices = [1, 2, 3]` and `fee = 2`:
|
|
- Greedy without fee thinking: Buy at 1, sell at 2 (profit 1), buy at 2, sell at 3 (profit 1). Total = 2.
|
|
- But with fee = 2, each transaction costs 2, so you'd lose money!
|
|
- Optimal: Buy at 1, sell at 3, one fee. Profit = (3 - 1) - 2 = 0.
|
|
|
|
The fee means you should **consolidate transactions** rather than making many small ones.
|
|
wrong_approach: "Greedily taking every price increase"
|
|
correct_approach: "Track states and let DP decide when selling is worthwhile"
|
|
|
|
- title: Forgetting State Dependency
|
|
description: |
|
|
When updating `hold` and `cash`, you might accidentally use the newly updated value instead of the old one.
|
|
|
|
For example, if you update `cash` first, then use that new `cash` to compute `hold`, you're allowing buying and selling on the same day, which is invalid.
|
|
|
|
Always compute new values using the old states, then assign them.
|
|
wrong_approach: "Sequential updates: cash = ..., hold = max(hold, cash - price)"
|
|
correct_approach: "Simultaneous updates using temp variables or tuple unpacking"
|
|
|
|
- title: Charging Fee Incorrectly
|
|
description: |
|
|
The fee should be charged exactly once per complete transaction (buy + sell). You can charge it either:
|
|
- When buying: `hold = cash - price - fee`
|
|
- When selling: `cash = hold + price - fee`
|
|
|
|
Either works, but be consistent. Charging on both would double-charge.
|
|
wrong_approach: "Charging fee on both buy and sell"
|
|
correct_approach: "Charge fee on exactly one of buy or sell"
|
|
|
|
key_takeaways:
|
|
- "**State machine DP**: Model problems with distinct states (holding/not holding) and transitions between them"
|
|
- "**Fee as a threshold**: Transaction costs create a minimum profit threshold that changes optimal decisions"
|
|
- "**O(1) space optimisation**: When DP only depends on the previous state, you can reduce from O(n) to O(1) space"
|
|
- "**Foundation for stock problems**: This pattern extends to problems with cooldowns, limited transactions, or other constraints"
|
|
|
|
time_complexity: "O(n). We iterate through the prices array exactly once, performing constant-time operations at each step."
|
|
space_complexity: "O(1). We only maintain two variables (`hold` and `cash`) regardless of input size."
|
|
|
|
solutions:
|
|
- approach_name: State Machine DP
|
|
is_optimal: true
|
|
code: |
|
|
def max_profit(prices: list[int], fee: int) -> int:
|
|
# hold: max profit if we currently own a stock
|
|
# cash: max profit if we don't own a stock
|
|
hold = -prices[0] # Buy on day 0
|
|
cash = 0 # Start with no profit
|
|
|
|
for i in range(1, len(prices)):
|
|
# To hold: either keep holding, or buy today
|
|
# To have cash: either keep cash, or sell today (pay fee)
|
|
hold, cash = (
|
|
max(hold, cash - prices[i]),
|
|
max(cash, hold + prices[i] - fee)
|
|
)
|
|
|
|
# End without holding stock for maximum profit
|
|
return cash
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Single pass through the prices array.
|
|
|
|
**Space Complexity:** O(1) — Only two variables used.
|
|
|
|
We track two states: the best profit when holding a stock, and when not holding. At each day, we compute the optimal way to reach each state. The tuple assignment ensures we use old values for both calculations. The fee is deducted when selling.
|
|
|
|
- approach_name: 2D DP Array
|
|
is_optimal: false
|
|
code: |
|
|
def max_profit(prices: list[int], fee: int) -> int:
|
|
n = len(prices)
|
|
# dp[i][0] = max profit on day i without holding stock
|
|
# dp[i][1] = max profit on day i while holding stock
|
|
dp = [[0, 0] for _ in range(n)]
|
|
|
|
dp[0][0] = 0 # Day 0, no stock
|
|
dp[0][1] = -prices[0] # Day 0, bought stock
|
|
|
|
for i in range(1, n):
|
|
# Not holding: either stayed that way, or sold today
|
|
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i] - fee)
|
|
# Holding: either stayed that way, or bought today
|
|
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
|
|
|
|
# Return max profit without holding stock
|
|
return dp[n-1][0]
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Single pass through the prices array.
|
|
|
|
**Space Complexity:** O(n) — DP array of size n with 2 states each.
|
|
|
|
This version explicitly stores the DP table, making the state transitions clearer. Each cell `dp[i][state]` represents the maximum profit achievable by day `i` in that state. The optimal solution above uses the observation that we only need the previous day's values.
|