---
title: "714. Best Time to Buy and Sell Stock with Transaction Fee"
url: "https://laigary.com/interview/coding/714-best-time-to-buy-and-sell-stock-with-transaction-fee"
type: "note"
section: "coding"
date: "2023-01-29"
updated: "2026-07-26"
tags: ["Greedy", "Dynamic Programming", "Classic"]
---

# 714. Best Time to Buy and Sell Stock with Transaction Fee

[714\. Best Time to Buy and Sell Stock with Transaction Fee](https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/)

交易次數無限，但**每完成一筆交易要付 `fee` 的手續費**。

## 思路

**這一題改的旋鈕是「每筆交易扣手續費」，交易次數仍然無限。** 完整的狀態機和其他五題的對照見 [股票買賣家族模板](/interview/coding/stock-template)。

從 [122](/interview/coding/122-best-time-to-buy-and-sell-stock-ii) 出發，唯一要改的是**在轉移式裡把 `fee` 減掉**：

```python
buys[i] = max(buys[i-1], sells[i-1] - prices[i] - fee)   # 買進時就先付掉手續費
```

**在買進時扣還是賣出時扣？** 兩種都對，只要**整份程式碼只扣一次**就好。

- **買進時扣**（這裡的寫法）：一買進就把手續費認列成成本
- **賣出時扣**：`sells[i] = max(sells[i-1], buys[i-1] + prices[i] - fee)`

差別只在中間狀態的數值，最終答案相同。買進時扣有個小好處：`sells` 的語意保持乾淨（就是「手上沒股票時的獲利」），不用記得它有沒有被扣過。

### 手續費改變了什麼

122 的貪心解（「把所有上漲區段吃下來」）**在這題不成立**。因為每拆一筆交易就多付一次手續費，`a → b → c` 拆成兩筆要付兩次費，可能還不如一次做完。

所以手續費的作用是**懲罰頻繁交易**，讓「要不要現在賣掉」變成一個真的要權衡的決定 —— 這正是狀態機存在的意義。這也是為什麼加了任何限制（冷凍期、手續費、次數上限）就必須回到 DP。

不過這題**還是有貪心解**，只是比 122 複雜：記住「有效買入價」，只在 `price > buy + fee` 時才賣，並在賣出後把買入價設成 `price - fee` 以允許連續上漲繼續累積。想得出來很漂亮，但推導比 DP 難，面試時給 DP 版比較穩。

## 解題方向

```python
class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        if len(prices) < 2:
            return 0

        buys = [float('-inf')] * len(prices) 
        sells = [0] * len(prices) 

        for i in range(len(prices)):
            if i == 0:
                buys[i] = max(buys[i], 0 - prices[i] - fee)
            else:
                buys[i] = max(buys[i-1], sells[i-1] - prices[i] - fee)
            sells[i] = max(sells[i-1], buys[i-1] + prices[i])
        return sells[-1]
```

和 [122](/interview/coding/122-best-time-to-buy-and-sell-stock-ii) 逐字比對，**只差 `- fee` 那兩處**。這就是把家族當成一個狀態機來記的好處：新題目來了只要問「規則改了哪一項」，然後改對應的那一行。

`buys` 初始化成 `-inf` 是哨兵（「還沒開始就持有股票」不是合法狀態），第 0 天的 `-prices[0] - fee` 則是真正的起點。

**兩個陣列可以省成兩個變數**，因為轉移只看前一天：

```python
        buys, sells = float('-inf'), 0
        for price in prices:
            buys = max(buys, sells - price - fee)
            sells = max(sells, buys + price)
        return sells
```

和 122 一樣，`sells` 用到同一輪剛更新的 `buys` 是無害的 —— 那代表「今天買今天賣」，獲利是 `-fee`，只會更差，`max` 不會選它。（[309 冷凍期](/interview/coding/309-best-time-to-buy-and-sell-stock-with-cool-down) 那題就不能這樣寫。）

## 補充

**整個家族的對照**見 [股票買賣家族模板](/interview/coding/stock-template)。這題和 [122](/interview/coding/122-best-time-to-buy-and-sell-stock-ii)、[309](/interview/coding/309-best-time-to-buy-and-sell-stock-with-cool-down) 是同一組（`k` 無限），三題的程式碼幾乎一樣：

| | 買進轉移 |
|---|---|
| [122](/interview/coding/122-best-time-to-buy-and-sell-stock-ii) | `max(buys, sells - price)` |
| [309](/interview/coding/309-best-time-to-buy-and-sell-stock-with-cool-down) | `max(buys, sells_前天 - price)` |
| **714 這題** | `max(buys, sells - price - fee)` |

一起看最有效率 —— 三題其實是同一題的三種微調。

## 複雜度

- 時間 $O(n)$ — 一趟迴圈，每天常數次計算
- 空間 $O(n)$ 用陣列、$O(1)$ 用滾動變數

其中 `n` 是天數。手續費不影響複雜度，只影響轉移式裡的一個常數項。
