Prefix Sum (前綴和)

Arrays & StringsPriority 4 of 5 — High value — a gap here costs you roundsHigh value 更新於 Sep 19, 2026
Section priorityPriority 5 of 5 — Must know — expect it in almost every loopMust knowPriority 4 of 5 — High value — a gap here costs you roundsHigh valuePriority 3 of 5 — Worth knowing — usually a variant of a must-know patternWorth knowingPriority 2 of 5 — Niche — read once, revisit only if a company is known to askNicheMarked on the sections that carry it — unmarked sections are background/reference.

範圍 — 前綴和/累積和 — 子陣列和、二維前綴和、前綴和搭配雜湊表做計數。 另見prefix_sum_advanced.md — 模板 9–14,也就是那些要借用其他資料結構的;prefix_sum_examples.md — 模板沒有直接解掉的實作題;difference_array.md — 區間更新而非區間查詢;binary_indexed_tree.md — 陣列本身也會變動時;kadane_algorithm.md — 不靠前綴和求最大子陣列;tree_backtrack.md — 模板 14 所一般化的那個 root→leaf 路徑 DFS。

LeetCode 題目清單

總覽

前綴和是一種預處理技巧:先花 O(n) 前處理,之後任何子陣列的和都能在 O(1) 時間算出來。核心想法就是預先算好「從陣列開頭累加到每個位置」的和。

關鍵性質

  • 時間複雜度
    • 前處理:O(n)
    • 查詢子陣列和:O(1)
    • 整體:O(n) 前處理 + 每次查詢 O(1)
  • 空間複雜度:O(n),用來存前綴和
  • 核心想法prefixSum[i] = nums[0] + nums[1] + ... + nums[i-1]
  • 子陣列和sum(i, j) = prefixSum[j+1] - prefixSum[i]
  • 什麼時候用
    • 有多次區間求和查詢
    • 帶條件的子陣列問題
    • 搭配 HashMap 把 O(n²) 降成 O(n)
    • 二維區間求和查詢

參考資料

題型分類

模式 1:基本區間求和 — LC 303

  • 說明:求任意區間 [i, j] 內元素的總和
  • 例子:LC 303 - Range Sum Query、LC 304 - Range Sum Query 2D
  • 模式:直接套前綴和公式
  • 關鍵洞見sum[i:j] = prefixSum[j+1] - prefixSum[i]

模式 2:子陣列和等於目標值 — LC 560

  • 說明:找出/計數總和等於目標值的子陣列
  • 例子:LC 560 - Subarray Sum Equals K、LC 325 - Maximum Size Subarray Sum Equals k
  • 模式:用 HashMap 存前綴和,然後檢查 (current_sum - target) 在不在裡面
  • 關鍵洞見:若 prefixSum[j] - prefixSum[i] = k,則 prefixSum[i] = prefixSum[j] - k

模式 3:帶整除/取餘的子陣列 — LC 523

  • 說明:牽涉到整除、餘數或取模運算的題目
  • 例子:LC 523 - Continuous Subarray Sum、LC 974 - Subarray Sums Divisible by K
  • 模式:HashMap 裡存的是餘數,不是實際的和
  • 關鍵洞見:若 (prefixSum[j] - prefixSum[i]) % k = 0,則 prefixSum[j] % k = prefixSum[i] % k

模式 4:區間加值/差分陣列 — LC 370

  • 說明:高效率地對陣列套用區間更新
  • 例子:LC 370 - Range Addition、LC 1094 - Car Pooling
  • 模式:差分陣列技巧搭配前綴和
  • 關鍵洞見:在起點加、在 end+1 減,最後再算一次前綴和

模式 5:二維前綴和 — LC 304

  • 說明:求二維矩陣中任意矩形區域的總和
  • 例子:LC 304 - Range Sum Query 2D、LC 1314 - Matrix Block Sum
  • 模式:建二維前綴和矩陣,用排容原理
  • 關鍵洞見sum = total - left - top + topleft

模式 6:先轉換再計數 — LC 1248

  • 說明:先把陣列元素轉換過,再用前綴和來計數
  • 例子:LC 1248 - Count Nice Subarrays、LC 926 - Flip String to Monotone
  • 模式:把元素轉成 0/1,再套帶條件的前綴和
  • 關鍵洞見:把題目轉化成更單純的前綴和問題

模式 7:距離總和(左右拆分) — LC 2615

  • 說明:高效率地算出索引之間絕對差值的總和
  • 例子:LC 2615 - Sum of Distances(LC 2121 - Intervals Between Identical Elements 是同一題)、LC 1685 - Sum of Absolute Differences、LC 2602 - Minimum Operations to Make All Array Elements Equal
  • 模式:先依值分組,再把每一組拆成左右兩半,套 count * value - sum 這條公式
  • 關鍵洞見:對組內序號 k 上的索引 idx,距離 = (idx * countLeft - sumLeft) + (sumRight - idx * countRight),而它可以收成 total - 2*prefix[k] + idx*(2*k - m)

模式 8:前綴最大值(貪婪分塊/分割) — LC 769

  • 說明:一路追蹤陣列的累積最大值。當 maxSoFar == i 時,前綴 [0..i] 剛好裝著 {0, 1, ..., i} 這些元素,可以獨立成一個排序區塊。
  • 例子:LC 769 - Max Chunks To Make Sorted、LC 768 - Max Chunks To Make Sorted II、LC 2012 - Sum of Beauty in the Array
  • 模式:單趟掃描搭配一個 maxSoFar 變數;每當 maxSoFar == currentIndex 就把區塊數加一
  • 關鍵洞見:因為陣列是 [0, n-1] 的一個排列,所以只要目前看過的最大值等於當前索引,位置 0..i 需要的所有值就一定已經在 arr[0..i]

模式 9:前綴 + 後綴拆分(在拆分點上做 minimax) — LC 2017

  • 說明:題目給的整個選擇空間塌縮成一個索引;它左邊是前綴和、右邊是後綴和,所以每個候選解都能在同一趟掃描裡算完
  • 例子:LC 2017 - Grid Game、LC 724 - Find Pivot Index、LC 1422 - Maximum Score After Splitting a String、LC 2483 - Minimum Penalty for a Shop
  • 模式:讓拆分點從左掃到右,手上握著兩個累加值 —— 比較之前先縮後綴、比較之後才長前綴,這樣拆分點那一格就不屬於任何一邊
  • 關鍵洞見:一旦「一個選擇」被化簡成「一個索引」,對所有選擇取 min/max 就只是 O(n) 的掃描,不是搜尋 —— 不需要 DP,也不需要圖論演算法(見模板 15)

0) 概念

前綴和陣列怎麼建(核心)

整套技巧都建立在一行核心程式上。把它背下來,其他都是推論:

python
for i in range(len(cnt)):
    prefix[i + 1] = prefix[i] + cnt[i]

一步一步來:

python
cnt = [1, 0, 1, 1, 1]

# Step 1: allocate size n+1, fill with 0
#   the leading prefix[0] = 0 is the "empty sum" sentinel
#   -> lets sum(0, r) work without a special case
prefix = [0] * (len(cnt) + 1)
# prefix = [0, 0, 0, 0, 0, 0]

# Step 2: each prefix[i+1] = running total up to (and including) cnt[i]
for i in range(len(cnt)):
    prefix[i + 1] = prefix[i] + cnt[i]

# prefix = [0, 1, 1, 2, 3, 4]

追蹤(為什麼索引是 i + 1 而不是 i):

text
cnt:        [ 1,  0,  1,  1,  1 ]
index i:      0   1   2   3   4

prefix[0] = 0                 ← sentinel (empty prefix)
prefix[1] = prefix[0] + cnt[0] = 0 + 1 = 1
prefix[2] = prefix[1] + cnt[1] = 1 + 0 = 1
prefix[3] = prefix[2] + cnt[2] = 1 + 1 = 2
prefix[4] = prefix[3] + cnt[3] = 2 + 1 = 3
prefix[5] = prefix[4] + cnt[4] = 3 + 1 = 4

prefix = [0, 1, 1, 2, 3, 4]
          ↑                 ↑
       empty sum        sum of ALL cnt

重點: prefixcnt 多一個元素。prefix[i+1] 回答的是 「前 i+1 個元素的和」= cnt[0] + ... + cnt[i]

一行版寫法itertools.accumulate 前面補一個 0):

python
from itertools import accumulate
prefix = list(accumulate(cnt, initial=0))   # [0, 1, 1, 2, 3, 4]

為什麼 sum(l, r) = prefix[r+1] - prefix[l]

text
Given nums:    [ a,  b,  c,  d,  e ]
Index:           0   1   2   3   4

Build prefix array (size n+1, prefix[0] = 0):

prefix[0] = 0
prefix[1] = a
prefix[2] = a + b
prefix[3] = a + b + c
prefix[4] = a + b + c + d
prefix[5] = a + b + c + d + e

Visual:

prefix:  0 |  a  | a+b | a+b+c | a+b+c+d | a+b+c+d+e |
index:   0    1      2      3        4          5

To get sum(l=1, r=3) = nums[1] + nums[2] + nums[3] = b + c + d:

prefix[r+1] = prefix[4] = a + b + c + d
prefix[l]   = prefix[1] = a
                           ─────────────
prefix[4] - prefix[1]   =     b + c + d  ✓

Visually (what gets cancelled out):

prefix[4]:  [ a | b | c | d ]
prefix[1]:  [ a ]
            ─────────────────
difference:     [ b | c | d ]   ← this is sum(1, 3)

為什麼大小是 n+1 多出來的 prefix[0] = 0 專門處理 l = 0 這個邊界情況:

text
sum(0, 2) = prefix[3] - prefix[0]
          = (a + b + c) - 0
          = a + b + c  ✓

沒有它的話,就得特別寫 if (left == 0) 的判斷(見 LC 303 的 V0)。

具體範例 — LC 303

text
nums = [-2, 0, 3, -5, 2, -1]

Step 1: Build prefix array
prefix = [0, -2, -2, 1, -4, -2, -3]
              ↑    ↑  ↑   ↑   ↑   ↑
              -2  -2+0 ...        sum of all

Step 2: Query
sumRange(0, 2) = prefix[3] - prefix[0] = 1 - 0 = 1       ✓  (-2+0+3)
sumRange(2, 5) = prefix[6] - prefix[2] = -3 - (-2) = -1  ✓  (3-5+2-1)
sumRange(0, 5) = prefix[6] - prefix[0] = -3 - 0 = -3     ✓  (-2+0+3-5+2-1)

兩種寫法比較

寫法 prefix 大小 建表 查詢 sum(l, r) 邊界情況
大小 n+1(推薦) n + 1 prefix[i+1] = prefix[i] + nums[i] prefix[r+1] - prefix[l] 不需要特例
大小 n n prefix[i] = prefix[i-1] + nums[i] prefix[r] - (l > 0 ? prefix[l-1] : 0) 需要 if (l == 0) 判斷

模板與演算法

模板比較表

模板類型 適用情境 關鍵資料結構 什麼時候用
基本前綴和 區間求和查詢 陣列 需要多次算區間和
HashMap + 前綴和 找目標和的子陣列 HashMap 找出/計數特定和的子陣列
取模前綴和 整除類問題 存餘數的 HashMap 子陣列和可被 k 整除
差分陣列 區間更新 標記起訖點的陣列 多次區間加值
二維前綴和 矩形求和查詢 二維矩陣 二維區間求和
距離總和 絕對差值的總和 HashMap + 前綴和 相同元素之間
前綴 + 後綴拆分 最佳拆分點/minimax 兩個累加值 整個答案由單一個索引決定

通用模板

python
def prefix_sum_solve(nums, target):
    """
    Universal prefix sum template for most problems
    """
    # Step 1: Initialize prefix sum and result
    prefix_sum = 0
    result = 0
    
    # Step 2: HashMap for storing prefix sums (if needed)
    prefix_map = {0: 1}  # Handle subarrays starting from index 0
    
    # Step 3: Iterate through array
    for num in nums:
        # Update prefix sum
        prefix_sum += num
        
        # Check condition based on problem type
        if prefix_sum - target in prefix_map:
            result += prefix_map[prefix_sum - target]
        
        # Update map
        prefix_map[prefix_sum] = prefix_map.get(prefix_sum, 0) + 1
    
    return result

模板 1:基本前綴和(區間查詢) — LC 303

python
class PrefixSum:
    def __init__(self, nums):
        """Build prefix sum array for range queries"""
        self.prefix = [0] * (len(nums) + 1)
        for i in range(len(nums)):
            self.prefix[i + 1] = self.prefix[i] + nums[i]
    
    def range_sum(self, left, right):
        """Get sum of elements from index left to right (inclusive)"""
        return self.prefix[right + 1] - self.prefix[left]
java
// Java implementation
class PrefixSum {
    private int[] prefix;
    
    public PrefixSum(int[] nums) {
        prefix = new int[nums.length + 1];
        for (int i = 0; i < nums.length; i++) {
            prefix[i + 1] = prefix[i] + nums[i];
        }
    }
    
    public int rangeSum(int left, int right) {
        return prefix[right + 1] - prefix[left];
    }
}

模板 2:HashMap + 前綴和(子陣列目標和) — LC 560

python
def subarray_sum_equals_k(nums, k):
    """Count subarrays with sum equal to k"""
    count = 0
    prefix_sum = 0
    prefix_map = {0: 1}  # Important: initialize with {0: 1}
    
    for num in nums:
        prefix_sum += num
        
        # Check if (prefix_sum - k) exists
        if prefix_sum - k in prefix_map:
            count += prefix_map[prefix_sum - k]
        
        # Update map
        prefix_map[prefix_sum] = prefix_map.get(prefix_sum, 0) + 1
    
    return count
java
// Java implementation
public int subarraySum(int[] nums, int k) {
    int count = 0, prefixSum = 0;
    Map<Integer, Integer> map = new HashMap<>();
    map.put(0, 1);  // Handle subarrays starting from index 0
    
    for (int num : nums) {
        prefixSum += num;
        
        if (map.containsKey(prefixSum - k)) {
            count += map.get(prefixSum - k);
        }
        
        map.put(prefixSum, map.getOrDefault(prefixSum, 0) + 1);
    }
    
    return count;
}

為什麼 map 存的是次數而不是索引 — 同一個前綴和可能在很多位置出現, 而每一個位置都能跟現在這裡構成一個合法的子陣列。存最新索引只會算到其中一個; 存「這個和出現過幾次」才會全部算到,所以更新寫成 map[sum] += 1, 而讀取寫成 count += map[sum - k]

模板 3:取模前綴和(整除類問題) — LC 974

核心數學洞見:

text
Let prefix[i] = sum of nums[0..i]

A subarray sum nums[j+1..i] is divisible by k:
  (prefix[i] - prefix[j]) % k == 0

This implies:
  prefix[i] % k == prefix[j] % k

So if we see the SAME remainder again at index i vs a previous index j,
the subarray nums[j+1..i] has sum divisible by k.

map stores: { remainder -> earliest index }

If the current remainder already exists in the map
AND the distance (i - map[remainder]) >= 2, we found a valid subarray.
python
def subarray_divisible_by_k(nums, k):
    """Count subarrays with sum divisible by k"""
    count = 0
    prefix_sum = 0
    remainder_map = {0: 1}  # remainder -> count
    
    for num in nums:
        prefix_sum += num
        remainder = prefix_sum % k
        
        # Handle negative remainders
        if remainder < 0:
            remainder += k
        
        if remainder in remainder_map:
            count += remainder_map[remainder]
        
        remainder_map[remainder] = remainder_map.get(remainder, 0) + 1
    
    return count

模板 4:差分陣列(區間更新) — LC 370

python
def range_addition(length, updates):
    """Apply multiple range additions efficiently"""
    # Step 1: Create difference array
    diff = [0] * (length + 1)
    
    # Step 2: Apply range updates to difference array
    for start, end, val in updates:
        diff[start] += val
        diff[end + 1] -= val
    
    # Step 3: Compute prefix sum to get final result
    result = []
    current_sum = 0
    for i in range(length):
        current_sum += diff[i]
        result.append(current_sum)
    
    return result

模板 5:二維前綴和 — LC 304

python
class NumMatrix:
    def __init__(self, matrix):
        """Build 2D prefix sum matrix"""
        if not matrix or not matrix[0]:
            return
        
        m, n = len(matrix), len(matrix[0])
        self.prefix = [[0] * (n + 1) for _ in range(m + 1)]
        
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                self.prefix[i][j] = (matrix[i-1][j-1] + 
                                   self.prefix[i-1][j] + 
                                   self.prefix[i][j-1] - 
                                   self.prefix[i-1][j-1])
    
    def sumRegion(self, row1, col1, row2, col2):
        """Calculate sum of rectangle from (row1,col1) to (row2,col2)"""
        return (self.prefix[row2+1][col2+1] - 
                self.prefix[row1][col2+1] - 
                self.prefix[row2+1][col1] + 
                self.prefix[row1][col1])

模板 6:先轉換再計數 — LC 1248

python
def count_nice_subarrays(nums, k):
    """Count subarrays with exactly k odd numbers"""
    # Transform: odd -> 1, even -> 0
    transformed = [1 if x % 2 == 1 else 0 for x in nums]

    # Now it's subarray sum equals k problem
    count = 0
    prefix_sum = 0
    prefix_map = {0: 1}

    for val in transformed:
        prefix_sum += val

        if prefix_sum - k in prefix_map:
            count += prefix_map[prefix_sum - k]

        prefix_map[prefix_sum] = prefix_map.get(prefix_sum, 0) + 1

    return count

你不一定要真的把轉換後的陣列建出來。 上面的 transformed 是為了讓 「奇數 → 1、偶數 → 0」這一步看得見,但在時間壓力下該寫的形式,是在維護前綴和的 那個迴圈裡直接測 x % 2 — 這個轉換是一個判斷式,不是一趟掃描。

模板 7:距離總和(左右拆分) — LC 2615

這個模式能高效率算出索引之間絕對差值 |i - j| 的總和。

核心想法

兩個動作,順序如下:

  1. 先把索引依值分組{value: [indices]}),因為 |i - j| 只會發生在相同的值之間 —— 每一組都是彼此獨立、自成一題的子問題。
  2. **在一組之內,靠拆分點把絕對值拿掉。**拆分點左邊全是 pivot - other,右邊全是 other - pivot;絕對值符號一個都不剩,而每一半都是 count * pivot ∓ sum —— 差一個前綴和而已。

**每一組的索引是免費附贈排序好的。**你是從左到右掃描時把 i 一個個 append 進去的,所以 每個清單本來就遞增。這正是整個做法能是 O(n) 的原因 —— 去排序那些組要花 O(n log n), 而且什麼也換不到。這裡千萬不要排序;一排序就露出了「沒搞懂分組這一步」。

推導

先講記號 —— 本節後面都照這套用

text
indices = [i_0, i_1, i_2, ..., i_{m-1}]   the sorted indices of ONE value
m       = len(indices)

For the element at RANK k inside that group:
  idx       = indices[k]      its position in the ORIGINAL array
  left_cnt  = k               how many equal values sit before it
  right_cnt = m - 1 - k       how many sit after it

Prefix sum over the group (size m+1, leading sentinel):
  prefix[0]     = 0                                  the empty sum
  prefix[k]     = i_0 + i_1 + ... + i_{k-1}          first k     → everything LEFT of rank k
  prefix[k + 1] = i_0 + i_1 + ... + i_k              first k + 1 → the left part, plus idx itself
  prefix[m]     = i_0 + i_1 + ... + i_{m-1}          the whole group

這裡有兩個不同的索引在跑,把它們搞混是最經典的失誤。k組內的序號 —— 它索引的是 indicesprefixidx = indices[k] 才是nums的位置 —— 它是被加總的那個值, 也是答案要寫回去的地方(res[idx],絕對不是 res[k])。

怎麼讀一個前綴索引 —— 整段推導就靠這一條規則

text
prefixSum[i] = nums[0] + nums[1] + ... + nums[i-1]

  -> so, when we say prefixSum[i],
     we are summing the values in [0, i-1]   ← i is EXCLUSIVE, i itself is NOT in the sum

Applied to a group's indices:

  -> i_0 + i_1 + ... + i_{k-1}          is   prefix[k]
     (everything strictly LEFT of rank k)          upper end k is excluded, so idx is out ✓

  -> i_{k+1} + i_{k+2} + ... + i_{m-1}  is   prefix[m] - prefix[k + 1]
     (everything strictly RIGHT of rank k)         whole group, minus the first k+1 (which ends AT idx)

具體例子 —— 組 [2, 5, 8, 12]m = 4,所以 prefix = [0, 2, 7, 15, 27]

text
rank k:      0   1   2    3
indices:     2   5   8   12
prefix:  0   2   7  15   27
         ↑                ↑
    prefix[0]         prefix[4] = prefix[m]

At rank k = 2  (idx = 8):

  left part  = i_0 + i_1                  = 2 + 5  = 7
             = prefix[k]  = prefix[2]     = 7                       ✓ (8 is NOT included)

  right part = i_3                        = 12
             = prefix[m] - prefix[k + 1]
             = prefix[4] - prefix[3]      = 27 - 15 = 12            ✓ (8 is NOT included)

Contrast the off-by-one:
  prefix[m] - prefix[k] = 27 - 7 = 20 = 8 + 12   ← still carries idx itself, hence prefix[k+1]

1) 左邊的距離 —— 左邊每個索引都比較小,所以 |idx - i| = idx - i,絕對值符號就掉了:

text
left = (idx - i_0) + (idx - i_1) + ... + (idx - i_{k-1})

     = (idx + idx + ... + idx)  -  (i_0 + i_1 + ... + i_{k-1})
       └─── k copies of idx ──┘     └── sum of the first k ──┘

     = idx * k - (i_0 + ... + i_{k-1})

     = idx * left_cnt - prefix[k]

2) 右邊的距離 —— 右邊每個索引都比較大,所以 |idx - i| = i - idx

text
right = (i_{k+1} - idx) + (i_{k+2} - idx) + ... + (i_{m-1} - idx)

      = (i_{k+1} + i_{k+2} + ... + i_{m-1})  -  (idx + ... + idx)
        └───── sum of the right part ──────┘     └ m-1-k copies ┘

      = (total_sum - sum_up_to_and_including_idx) - idx * (m - 1 - k)

      = (prefix[m] - prefix[k + 1]) - idx * right_cnt

為什麼右半減的是 prefix[k + 1] 而不是 prefix[k]prefix[k] 停在 idx 之前, 所以 prefix[m] - prefix[k] 裡面還含著 idx;把它配上 right_cnt = m - 1 - k, 答案就剛好多算了一個 idx。總和與個數必須對「拆分點算不算在右半」這件事有一致的看法; prefix[k + 1]m - 1 - k 說的就是「不算」。

3) 總距離

text
res[idx] = left + right
         = (idx * left_cnt - prefix[k])  +  ((prefix[m] - prefix[k + 1]) - idx * right_cnt)

在對每一組做完 O(m) 的前綴掃描之後,每個元素只要 O(1),所以整體是 O(n) —— 相對於逐對比較的 O(n^2)

圖解

text
One group's indices: [2, 5, 8, 12]        m = 4
rank k:               0  1  2   3

prefix = [0, 2, 7, 15, 27]
          ↑                ↑
       empty sum        prefix[m] = whole group

Take rank k = 2  →  idx = 8, left_cnt = 2, right_cnt = m-1-k = 1

  left  = idx * left_cnt - prefix[k]
        = 8 * 2 - prefix[2]                    prefix[2] = 2 + 5 = 7
        = 16 - 7 = 9                           → |8-2| + |8-5| = 6 + 3 = 9 ✓

  right = (prefix[m] - prefix[k+1]) - idx * right_cnt
        = (27 - 15) - 8 * 1                    prefix[3] = 2 + 5 + 8 = 15
        = 12 - 8 = 4                           → |12-8| = 4 ✓

  res[8] = 9 + 4 = 13

用 LC 2615 的例子從頭走到尾 —— nums = [1,3,1,1,2],預期答案 [5,0,3,4,0]

text
groups:  1 -> [0, 2, 3]      3 -> [1]      2 -> [4]

Group [0, 2, 3]:  m = 3,  prefix = [0, 0, 2, 5]

 k=0  idx=0  left_cnt=0 right_cnt=2   left = 0*0 - prefix[0] = 0
                                      right = (5 - prefix[1]) - 0*2 = 5 - 0 = 5   res[0] = 5 ✓
 k=1  idx=2  left_cnt=1 right_cnt=1   left = 2*1 - prefix[1] = 2 - 0 = 2
                                      right = (5 - prefix[2]) - 2*1 = 3 - 2 = 1   res[2] = 3 ✓
 k=2  idx=3  left_cnt=2 right_cnt=0   left = 3*2 - prefix[2] = 6 - 2 = 4
                                      right = (5 - prefix[3]) - 3*0 = 0           res[3] = 4 ✓

Single-index groups keep res = 0  →  res[1] = res[4] = 0 ✓

Python 模板

python
def sum_of_distances(nums):
    """
    LC 2615: Calculate sum of |i - j| for all j where nums[j] == nums[i]
    Time: O(n), Space: O(n)
    """
    from collections import defaultdict

    n = len(nums)
    result = [0] * n

    # Step 1: Group indices by value
    index_map = defaultdict(list)
    for i, num in enumerate(nums):
        index_map[num].append(i)

    # Step 2: For each group, calculate distances using prefix sum
    for indices in index_map.values():
        m = len(indices)
        if m == 1:
            continue  # Single element has distance 0

        # prefix[k] = sum of the first k indices  (size m+1, prefix[0] = 0)
        prefix = [0] * (m + 1)
        for k in range(m):
            prefix[k + 1] = prefix[k] + indices[k]

        # Calculate distance for each index in group
        for k in range(m):
            idx = indices[k]

            # Left part: idx * left_cnt - prefix[k]
            left_cnt = k
            left_dist = idx * left_cnt - prefix[k]

            # Right part: (prefix[m] - prefix[k+1]) - idx * right_cnt
            right_cnt = m - 1 - k
            right_dist = (prefix[m] - prefix[k + 1]) - idx * right_cnt

            result[idx] = left_dist + right_dist

    return result

Java 模板

java
// LC 2615 - Sum of Distances
public long[] distance(int[] nums) {
    int n = nums.length;
    long[] res = new long[n];
    Map<Integer, List<Integer>> map = new HashMap<>();

    // Step 1: Group indices by value
    for (int i = 0; i < n; i++) {
        map.computeIfAbsent(nums[i], k -> new ArrayList<>()).add(i);
    }

    // Step 2: Calculate distances using prefix sum
    for (List<Integer> indices : map.values()) {
        int m = indices.size();
        if (m == 1) continue;

        // prefix[k] = sum of the first k indices  (size m+1, prefix[0] = 0)
        long[] prefix = new long[m + 1];
        for (int k = 0; k < m; k++) {
            prefix[k + 1] = prefix[k] + indices.get(k);
        }

        // Calculate distance for each index
        for (int k = 0; k < m; k++) {
            int idx = indices.get(k);

            // Left: idx * left_cnt - prefix[k]
            long left = (long) idx * k - prefix[k];

            // Right: (prefix[m] - prefix[k+1]) - idx * right_cnt
            long right = (prefix[m] - prefix[k + 1]) - (long) idx * (m - 1 - k);

            res[idx] = left + right;
        }
    }

    return res;
}

替代作法:邊走邊累加(不建前綴陣列)

python
def sum_of_distances_optimized(nums):
    """Space-optimized version using running sums"""
    from collections import defaultdict

    n = len(nums)
    result = [0] * n
    index_map = defaultdict(list)

    for i, num in enumerate(nums):
        index_map[num].append(i)

    for indices in index_map.values():
        m = len(indices)
        if m == 1:
            continue

        # Calculate total sum once
        total_sum = sum(indices)

        prefix_sum = 0
        for i, idx in enumerate(indices):
            # Left: idx * i - prefix_sum
            # Right: (total_sum - prefix_sum - idx) - idx * (m - i - 1)
            left_dist = idx * i - prefix_sum
            right_dist = (total_sum - prefix_sum - idx) - idx * (m - i - 1)

            result[idx] = left_dist + right_dist
            prefix_sum += idx

    return result

同一件事寫成一行(值得背下來的恆等式)

左右拆分是你在白板上推導答案的方式,但這兩半在代數上可以收成單一個運算式 —— 沒有分支,也不必維護 countLeft / countRight

text
left  = idx * i           - prefixSum
right = (total - prefixSum - idx) - idx * (m - i - 1)

left + right
  = idx*i - prefixSum + total - prefixSum - idx - idx*m + idx*i + idx
  = total - 2 * prefixSum + idx * (2*i - m)
                             ↑
                    the -idx and +idx cancel
python
# python
# LC 2615 - Sum of Distances  (collapsed form)
# IDEA: total - 2*prefix[k] + idx*(2k - m) is exactly leftDist + rightDist  (k = rank, m = group size)
# time = O(n), space = O(n)
from collections import defaultdict

def distance(nums):
    groups = defaultdict(list)
    for i, v in enumerate(nums):
        groups[v].append(i)

    res = [0] * len(nums)
    for group in groups.values():
        total, prefix_sum, m = sum(group), 0, len(group)
        for i, idx in enumerate(group):
            res[idx] = total - prefix_sum * 2 + idx * (2 * i - m)
            prefix_sum += idx
    return res

**m == 1 的特判就不需要了。**對只出現一次的組,這條公式自己就會算出 idx - 0 + idx*(0 - 1) = 0,正好是題目要的答案。拆分寫法之所以需要 if m == 1: continue,只是因為它被寫成了兩塊。

公式整理

在大小為 m 的組裡、序號 k 的位置上,令 idx = indices[k]prefix 是長度 m+1 的前綴和:

部分 公式 意義
左側個數/總和 left_cnt = ksum_left = prefix[k] idx 之前那 k 個相同的值
右側個數/總和 right_cnt = m - 1 - ksum_right = prefix[m] - prefix[k + 1] idx 之後那些相同的值,不含拆分點本身
左側距離 idx * left_cnt - prefix[k] (idx - smaller_idx) 的總和
右側距離 (prefix[m] - prefix[k + 1]) - idx * right_cnt (larger_idx - idx) 的總和
總距離 left + rightres[idx] 所有 |idx - other_idx| 的總和

類似題 —— count * value − sum 家族

每一題都是同一條恆等式;變的只是那個排序清單裝什麼,以及它從哪來

題目 排序清單裝的是… 和 LC 2615 的差別
LC 2121 Intervals Between Identical Elements 依值分組後的索引 **完全同一題。**LC 2615 的題敘自己就這麼說 —— 同樣的輸入、同樣的輸出,只是換了標題
LC 1685 Sum of Absolute Differences in a Sorted Array 那些,而且本來就排好了 不用分組也不用雜湊表 —— 陣列本身就是那一組,所以它是「只有一組」的情況
LC 2602 Minimum Operations to Make All Array Elements Equal 排序後的值,外加一個前綴陣列 拆分點是查詢值而不是某個元素,所以先二分搜出它的插入位置,然後套同樣的兩半
LC 2448 Minimum Cost to Make Array Equal 排序後的值,各自帶權重 每個元素算 w 次:個數變成權重和,所以要對 ww*v 各做一份前綴和
LC 462 Minimum Moves to Equal Array Elements II 排序後的值 只問所有拆分點裡的最小值,而那就是中位數 —— 連逐元素掃描都不用
LC 834 Sum of Distances in Tree 「那條線」變成一棵樹,所以左右拆分變成「子樹/其餘部分」,靠換根去算

**在面試現場怎麼認出它。**觸發條件是「在一個你可以排序的集合上求 |x − y| 的總和」。 排序把絕對值拿掉 —— 拆分點之前的都是減、之後的都是加 —— 而絕對值符號一消失, 每一半就是 count × pivot ∓ sum,前綴和 O(1) 就回答得出來。 把這句話講出來,O(n^2) 的暴力解就已經被你甩在後面了。

模板 8:前綴最大值(貪婪分塊/分割) — LC 769

核心想法: 對於 [0, n-1] 的一個排列,前綴 arr[0..i] 能獨立成一個排序區塊,當且僅當 max(arr[0..i]) == i。用一個 maxSoFar 變數就能追蹤這件事。

java
// Java — LC 769 Max Chunks To Make Sorted
// Time: O(n)  Space: O(1)
public int maxChunksToSorted(int[] arr) {
    int chunks = 0, maxSoFar = 0;
    for (int i = 0; i < arr.length; i++) {
        maxSoFar = Math.max(maxSoFar, arr[i]);
        if (maxSoFar == i) chunks++;   // all values 0..i are present in arr[0..i]
    }
    return chunks;
}
python
# Python — LC 769
def maxChunksToSorted(arr):
    chunks = max_so_far = 0
    for i, val in enumerate(arr):
        max_so_far = max(max_so_far, val)
        if max_so_far == i:
            chunks += 1
    return chunks

等價的前綴和寫法(同樣是 O(n)/O(1)):

java
// prefixSum of arr == prefixSum of sorted arr  →  same multiset in [0..i]
int chunks = 0, prefixSum = 0, sortedPrefixSum = 0;
for (int i = 0; i < arr.length; i++) {
    prefixSum += arr[i];
    sortedPrefixSum += i;           // sorted array is [0,1,2,...,n-1]
    if (prefixSum == sortedPrefixSum) chunks++;
}

**為什麼 LC 769 光比總和就夠。**因為值是 0..n-1 的一個排列,所以 arr 的某段前綴 只有在裝著跟排序後陣列同長度前綴「同一組值」(順序可以不同)時,兩者的和才會相等。 而那正是「這段前綴自成一個區塊」的條件 —— 所以這個和的檢查根本不需要排序。

什麼時候該升級成 PrefixMax + SuffixMin(LC 768,一般陣列):

java
// If values are NOT a permutation, use:
// max(arr[0..i-1]) < min(arr[i..n-1])  →  valid cut point
int[] prefixMax = arr.clone(), suffixMin = arr.clone();
for (int i = 1; i < n; i++) prefixMax[i] = Math.max(prefixMax[i-1], prefixMax[i]);
for (int i = n-2; i >= 0; i--) suffixMin[i] = Math.min(suffixMin[i+1], suffixMin[i]);
int chunks = 0;
for (int i = 0; i < n; i++)
    if (i == 0 || suffixMin[i] > prefixMax[i-1]) chunks++;

**同一對陣列,問的卻是元素而不是切點。**LC 2012 一樣留著 prefixMaxsuffixMin,但它檢查的是 prefixMax[i] < nums[i] < suffixMin[i],而且只有後綴那一側 需要真的開陣列 —— 完整走過一遍在 prefix_sum_examples.md § Prefix max / suffix min scans

模板 15:前綴 + 後綴拆分 — 在拆分點上做 minimax — LC 2017Priority 4 of 5 — High value — a gap here costs you rounds

之所以編號 15,是因為 9–14 是下一節的進階組;這一個屬於核心模板。

核心想法

有些題目讀起來像是要在路徑或分割方式上做搜尋,但整個選擇空間其實塌縮成一個索引。 一旦如此,這個索引的兩側就是一個前綴和與一個後綴和,而最佳選擇只要一趟線性掃描 —— 不用 DP,也不用圖論搜尋。

LC 2017 是最乾淨的例子。格子是 2 x n,合法路徑是沿著 row 0 往右走、恰好往下掉一次、 再沿著 row 1 往右走 —— 所以一條路徑就等於它往下轉彎的那一欄 i。 路徑只有 n 條,不是指數多條。

text
grid = [[2, 5, 4],
        [1, 5, 1]]        robot 1 turns down at column i = 1

        col:   0     1     2
row 0:       [ 2 ] [ 5 ]   4        4 survives   ->  TOP    = suffix of row 0
row 1:         1   [ 5 ] [ 1 ]      1 survives   ->  BOTTOM = prefix of row 1
                     ^
        [ ] = zeroed by robot 1     column i is on robot 1's own path -> in NEITHER block

機器人 1 在 i 轉彎,會把 row0[0..i]row1[i..n-1] 清成 0,於是留給機器人 2 的 剛好是兩塊沒被碰過的區域 —— 而它們落在 i兩側

區塊 格子 是哪種和
row0[i+1 ... n-1] row 0 的後綴
row1[0 ... i-1] row 1 的前綴

機器人 2 同樣只掉一次,所以它只能拿到兩塊中的一塊;而因為每個值都 >= 1, 它會把那一塊整塊拿走(要上面那塊就在第 n-1 欄掉,要下面那塊就在第 0 欄掉)。 所以機器人 2 拿到 max(top, bottom),而機器人 1 要挑讓它最小的那個 i

text
answer = min over i of   max( sum(row0[i+1 ... n-1]) ,  sum(row1[0 ... i-1]) )
                              └──── suffix, shrinks ─┘   └──── prefix, grows ─┘

模式

兩個累加值往相反方向移動,而迴圈裡那三行的順序就是整個模板

text
top    = sum(row 0)          the suffix, starts whole
bottom = 0                   the prefix, starts empty

for i in 0 .. n-1:
    top    -= row0[i]        1. shrink the suffix FIRST  -> column i leaves the top block
    res     = min(res, max(top, bottom))   2. score this split
    bottom += row1[i]        3. grow the prefix AFTER    -> column i joins the bottom block
                                              only for the NEXT split

把第 3 步放到第 2 步之前,第 i 欄就被重複算進下面那塊;把第 1 步放到第 2 步之後, 它就還留在上面那塊裡。兩種失誤都會去評分一條根本不存在的路徑。

python
# python
# LC 2017 - Grid Game
# IDEA: a path in a 2 x n grid IS its turning column, so scan the column and keep
#       suffix(row 0) and prefix(row 1) as two running sums
# time = O(n), space = O(1)
class Solution(object):
    def gridGame(self, grid):
        top = sum(grid[0])      # suffix row0[i+1 ... n-1] once shrunk
        bottom = 0              # prefix row1[0   ... i-1] once grown
        res = float('inf')

        for i in range(len(grid[0])):
            top -= grid[0][i]                    # column i leaves the top block
            res = min(res, max(top, bottom))     # robot 2 takes the bigger block
            bottom += grid[1][i]                 # column i joins the bottom block

        return res
java
// java
// LC 2017 - Grid Game
// IDEA: minimise, over the turning column, the max of (row 0 suffix, row 1 prefix)
// time = O(n), space = O(1)
public long gridGame(int[][] grid) {
    long top = 0, bottom = 0, res = Long.MAX_VALUE;
    for (int v : grid[0]) top += v;              // n <= 5e4, v <= 1e5 -> 5e9, so long

    for (int i = 0; i < grid[0].length; i++) {
        top -= grid[0][i];
        res = Math.min(res, Math.max(top, bottom));
        bottom += grid[1][i];
    }
    return res;
}

步驟追蹤 — grid = [[2,5,4],[1,5,1]]

text
top starts at 2+5+4 = 11, bottom at 0

 i | top -= row0[i] | bottom | max(top, bottom) | res | bottom += row1[i]
---+----------------+--------+------------------+-----+------------------
 0 | 11 - 2 =  9    |   0    |        9         |  9  | 0 + 1 = 1
 1 |  9 - 5 =  4    |   1    |        4    <--  |  4  | 1 + 5 = 6
 2 |  4 - 4 =  0    |   6    |        6         |  4  | 6 + 1 = 7

answer = 4   (robot 1 turns at column 1, robot 2 takes the lone 4 on the top row) ✓

陷阱

  • **這是 minimax,不是貪婪。**機器人 1 並不是在最大化自己拿到的分數。抓走最肥的那條路徑, 可能反而留給機器人 2 更肥的殘局 —— 目標函數是 min(max(...)),而只有掃過全部 n 個 拆分點才看得出這件事。
  • **不要去搬圖論演算法。**Dijkstra/最長路徑回答的是「對我最好的路線」,那是錯的目標函數; 而且 2 x n 這個形狀本來就沒什麼可搜的:整個決策就是一個欄位索引。
  • **拆分點那一格不屬於任何一邊。**見上面關於順序的說明 —— 那是這個模板唯一真正的 bug 來源。
  • Java 會溢位。n <= 5 * 10^4、值 <= 10^5,一整列的總和就到 5 * 10^9; 請用 long 累加。

類似題 —— 拆分點家族

每次都是同一個形狀:一個索引決定答案,它左邊是前綴、右邊是後綴。

題目 LC # 拆分索引是什麼 和 LC 2017 的差別
Find Pivot Index 724 那個 pivot 最素的情況:找出前綴 == 後綴的切點,沒有 min/max
Product of Array Except Self 238 每一個索引輪流當 前綴乘積 × 後綴乘積,而不是和
Maximum Score After Splitting a String 1422 那一刀 最大化「左邊的 0」+「右邊的 1」—— 一趟掃描,同樣兩個計數器
Minimum Penalty for a Shop 2483 打烊的那個小時 最小化「之前流失的客人」+「之後沒客人的時數」
Flip String to Monotone Increasing 926 0 → 1 的分界 最小化「左邊的 1」+「右邊的 0」;本來就是模板 6 的轉換
Partition Array Into Three Parts With Equal Sum 1013 兩刀 前綴必須兩次命中 total/3 —— 一趟掃描帶兩個檢查點
Ways to Split Array Into Three Subarrays 1712 兩刀 第二刀相對第一刀是單調的,所以拿二分搜去找它
Trapping Rain Water 42 每一個索引 用前綴最大值/後綴最大值而不是和(對比模板 8)
Maximum Trailing Zeros in a Cornfield Path 2245 格子裡轉彎的那一格 LC 2017 的「轉一次」搬到完整格子上:四個方向,對因數 2 與 5 各做前綴計數

**在面試現場怎麼認出它。**問自己:*真正不同的選擇到底有幾種?*如果答案是「每個索引一種」—— 一個轉彎欄、一刀、一個 pivot —— 就別再找 DP 了,直接寫那兩個累加值的掃描。 LC 2017 的破綻是 grid.length == 2:兩列加上只能掉一次,代表一條路徑再沒有別的自由度。

進階模板

模板 9–14 搬到 prefix_sum_advanced.md 了。它們是那些 已經不只是「建個陣列、相減兩項」,而是開始借用其他資料結構的模板:

# 模板 借來的想法 LC
9 補集技巧 — 總和 − 中間視窗 ⭐⭐⭐⭐⭐ 頭尾繞回來的選法,等於一段要排除的連續視窗 1423
10 前綴和 + 單調雙端佇列 用雙端佇列,因為負數會讓雙指標視窗失效 862
11 列對壓縮 ⭐⭐⭐⭐ 固定一對列,把二維壓成一維 363, 1074
12 前綴 XOR ⭐⭐⭐⭐ XOR 的反運算是自己,所以同一條相減恆等式仍然成立 1310
13 用 HashMap 做稀疏差分陣列 ⭐⭐⭐⭐⭐ 座標範圍太大時,用雜湊表取代陣列 2021
14 樹上的前綴和 ⭐⭐⭐⭐⭐ DFS 的堆疊就是那個陣列 —— 模板 2 再加上回溯時的復原 437

依模式分類的題目

按模式分類的題目清單

模式 1:基本區間求和

題目 LC # 關鍵技巧 難度 模板
Range Sum Query - Immutable 303 基本前綴和陣列 Easy 模板 1
Range Sum Query 2D - Immutable 304 二維前綴和 Medium 模板 5
Product of Array Except Self 238 左右前綴乘積 Medium 模板 1 改寫
Running Sum of 1d Array 1480 直接前綴和 Easy 模板 1
Find Pivot Index 724 左邊和 vs 右邊和 Easy 模板 1

模式 2:子陣列和等於目標值

題目 LC # 關鍵技巧 難度 模板
Subarray Sum Equals K 560 HashMap + 前綴和 Medium 模板 2
Maximum Size Subarray Sum Equals k 325 HashMap 存索引 Medium 模板 2
Subarray Sum Equals K II 713 乘積版本 Medium 模板 2 改寫
Binary Subarrays With Sum 930 轉換成求和等於目標 Medium 模板 6
Number of Subarrays with Bounded Maximum 795 區間求和技巧 Medium 模板 2
Longest Well-Performing Interval 1124 首次出現 map + 分數 ±1 技巧 Medium 模板 2 變形

模式 3:帶整除/取餘的子陣列

題目 LC # 關鍵技巧 難度 模板
Subarray Sums Divisible by K 974 取模前綴和 Medium 模板 3
Continuous Subarray Sum 523 取模再檢查長度 Medium 模板 3
Make Sum Divisible by P 1590 進階取模技巧 Medium 模板 3
Check If Array Pairs Are Divisible by k 1497 統計餘數頻率 Medium 模板 3 改寫

模式 4:區間加值/更新

題目 LC # 關鍵技巧 難度 模板
Range Addition 370 差分陣列 Medium 模板 4
Car Pooling 1094 時間軸模擬 Medium 模板 4
Corporate Flight Bookings 1109 區間更新 Medium 模板 4
Maximum Population Year 1854 事件處理 Easy 模板 4
Meeting Rooms II 253 重疊計數 Medium 模板 4
Brightest Position on Street 2021 稀疏差分陣列(HashMap) Medium 模板 13
Describe the Painting 1943 稀疏差分陣列(HashMap) Medium 模板 13

模式 5:二維矩陣

題目 LC # 關鍵技巧 難度 模板
Range Sum Query 2D 304 二維前綴和 Medium 模板 5
Matrix Block Sum 1314 二維區間查詢 Medium 模板 5
Number of Submatrices That Sum to Target 1074 二維 + HashMap Hard 模板 5 + 2
Maximum Side Length Square 1292 二分搜尋 + 二維前綴和 Medium 模板 5

模式 6:先轉換再計數

題目 LC # 關鍵技巧 難度 模板
Count Number of Nice Subarrays 1248 轉成奇偶 Medium 模板 6
Flip String to Monotone Increasing 926 轉成 0/1 再計數 Medium 模板 6
Max Chunks To Make Sorted 769 比較總和 Medium 模板 6
Longest Arithmetic Subsequence 1027 轉成差值 Medium 模板 6

模式 7:距離總和

題目 LC # 關鍵技巧 難度 模板
Sum of Distances 2615 分組 + 左右拆分 Medium 模板 7
Intervals Between Identical Elements 2121 和 2615 完全同一題,只是換標題 Medium 模板 7
Sum of Absolute Differences in a Sorted Array 1685 只有一組 —— 陣列本來就排好,不用 map Medium 模板 7
Minimum Operations to Make All Array Elements Equal 2602 拆分點是查詢值:先二分搜出它的序號,再套同樣的兩半 Medium 模板 7 + 二分搜尋
Minimum Cost to Make Array Equal 2448 帶權重 —— 對 ww*v 各做前綴和 Hard 模板 7 加權版
Minimum Moves to Equal Array Elements II 462 只要最好的那個拆分點,而那就是中位數 Medium 模板 7(中位數捷徑)
Sum of Distances in Tree 834 樹上的版本(DFS + 換根) Hard 模板 7 + DFS
Minimum Total Distance Traveled 2463 DP + 距離計算 Hard 模板 7 + DP

模式 8:前綴最大值

題目 LC # 關鍵技巧 難度 模板
Max Chunks To Make Sorted 769 前綴最大值 == 索引 Medium 模板 8
Max Chunks To Make Sorted II 768 PrefixMax + SuffixMin 陣列 Hard 模板 8
Find the Longest Turbulent Subarray 978 邊走邊追蹤狀態 Medium 模板 8 改寫
Sum of Beauty in the Array 2012 PrefixMax + SuffixMin,逐元素判斷 Medium 模板 8 變形

模式 9:前綴 + 後綴拆分

題目 LC # 關鍵技巧 難度 模板
Grid Game 2017 轉彎欄 + 後綴(row 0)/前綴(row 1),minimax Medium 模板 15
Find Pivot Index 724 找前綴 == 後綴的切點 Easy 模板 15(最素的情況)
Product of Array Except Self 238 前綴乘積 × 後綴乘積 Medium 模板 15(乘積版)
Maximum Score After Splitting a String 1422 最大化「左邊的 0」+「右邊的 1」 Easy 模板 15
Minimum Penalty for a Shop 2483 最小化「之前流失」+「之後空轉」 Medium 模板 15
Partition Array Into Three Parts With Equal Sum 1013 total/3 處切兩刀 Easy 模板 15(兩刀)
Ways to Split Array Into Three Subarrays 1712 兩刀,第二刀用二分搜 Medium 模板 15 + 二分搜尋
Trapping Rain Water 42 每個索引的前綴最大值/後綴最大值 Hard 模板 15(取 max 而非和)
Maximum Trailing Zeros in a Cornfield Path 2245 在格子裡轉一次,對 2 與 5 做前綴計數 Medium 模板 15(格子版)

進階/混合模式

題目 LC # 關鍵技巧 難度 模板
Maximum Sum of Two Non-Overlapping Subarrays 1031 多個前綴陣列 Medium 模板 1 + DP
Subarrays with K Different Integers 992 「最多 K 個」技巧 Hard 模板 2
Minimum Window Subsequence 727 滑動視窗 + 前綴和 Hard 模板 2 + SW
Split Array With Same Average 805 子集合和問題 Hard 模板 2
Largest Rectangle in Histogram 84 堆疊 + 前綴和 Hard 模板 1 + 堆疊

補充練習題

Easy(打底)

題目 LC # 重點 模板
Two Sum 1 HashMap 基本功 模板 2 改寫
Contains Duplicate II 219 滑動視窗 + map 模板 2
Maximum Average Subarray I 643 固定長度子陣列 模板 1
Degree of an Array 697 元素頻率 模板 2

Medium(核心模式)

題目 LC # 重點 模板
Contiguous Array 525 平衡 0 和 1 模板 6
Shortest Unsorted Continuous Subarray 581 陣列分析 模板 1
Random Pick with Weight 528 帶權重的隨機選取 模板 1
Path Sum III 437 樹 + 前綴和 模板 14

Hard(進階技巧)

題目 LC # 重點 模板
Count of Range Sum 327 合併排序 + 前綴和 進階
Reverse Pairs 493 合併排序技巧 進階
Create Maximum Number 321 貪婪 + 前綴 進階
Count Different Palindromic Subsequences 730 DP + 前綴 進階

模式選擇策略

前綴和題目的決策框架

text
Problem Analysis Flowchart:

1. Need multiple range sum queries?
   ├── YES → Use Template 1 (Basic Prefix Sum)
   └── NO → Continue to 2

2. Looking for subarrays with specific sum/count?
   ├── YES → Continue to 2a
   └── NO → Continue to 3
   
   2a. Exact sum target?
       ├── YES → Use Template 2 (HashMap + Prefix Sum)
       └── NO → Continue to 2b
   
   2b. Divisibility or modulo involved?
       ├── YES → Use Template 3 (Modulo Prefix Sum)
       └── NO → Continue to 2c
   
   2c. Count odd/even or binary transformation?
       ├── YES → Use Template 6 (Transform and Count)
       └── NO → Use Template 2

3. Multiple range updates needed?
   ├── YES → Use Template 4 (Difference Array)
   └── NO → Continue to 4

4. 2D matrix operations?
   ├── YES → Use Template 5 (2D Prefix Sum)
   └── NO → Continue to 5

5. Is every candidate answer described by ONE index (a cut, a pivot, a turning column)?
   ├── YES → Use Template 15 (Prefix + Suffix Split), even when the ask is min(max(...))
   └── NO → Continue to 6

6. Special cases:
   ├── Product instead of sum → Modified Template 1
   ├── Tree path sums → Template 2 + Tree traversal
   ├── Sliding window + prefix → Combine templates
   └── Advanced merge/sort → Custom approach

模板選擇指南

題目關鍵字 建議模板 例題
「range sum」、「query」 模板 1 LC 303, 304
「subarray sum equals」、「count subarrays」 模板 2 LC 560, 325
「divisible by」、「remainder」、「modulo」 模板 3 LC 974, 523
「range addition」、「updates」、「intervals」 模板 4 LC 370, 1094
「2D」、「matrix」、「rectangle」 模板 5 LC 304, 1314
「odd numbers」、「binary」、「transform」 模板 6 LC 1248, 926
「sum of distances」、「absolute differences」、「identical elements」,以及任何在可排序集合上求 sum of |x-y| 模板 7 LC 2615, 2121, 1685, 2602
「max chunks」、「partition to sort」、「split into sorted segments」 模板 8 LC 769, 768
「take from both ends」、「remove from left or right」 模板 9 LC 1423, 1658
「shortest subarray with sum ≥ K」且允許負數 模板 10 LC 862(對比 LC 209 的視窗解)
「submatrix sum ≤ k」、「count submatrices」、「rectangle + condition」 模板 11 LC 363, 1074
「XOR of subarray」、「even count of every letter」、「parity」 模板 12 LC 1310, 1915, 1738
「2 x n grid」、「one turn」、「best split point」、「both play optimally」 模板 15 LC 2017, 724, 1422, 2483

模板 9–13 的完整內容寫在 prefix_sum_advanced.md

怎麼認出各個模板

認出該用模板 1:

  • 題目提到:「range sum query」、「immutable array」、「multiple queries」
  • 輸入:陣列 + 多組 (left, right) 查詢
  • 輸出:區間 [left, right] 內元素的總和

認出該用模板 2:

  • 題目提到:「subarray sum equals K」、「count subarrays」、「target sum」
  • 關鍵洞見:要找出滿足 prefixSum[j] - prefixSum[i] = target 的 (i, j) 配對
  • HashMap 存的是:{prefixSum: count}{prefixSum: index}

認出該用模板 3:

  • 題目提到:「divisible by K」、「remainder」、「modulo」、「continuous sum」
  • 關鍵洞見:(prefixSum[j] - prefixSum[i]) % k = 0 代表兩者餘數相同
  • HashMap 存的是:{remainder: count}{remainder: index}

認出該用模板 4:

  • 題目提到:「range updates」、「add value to range」、「difference array」
  • 有多次這種操作:「對索引 [start, end] 加上 val」
  • 關鍵洞見:先標記起訖點,最後再算前綴和
  • 如果座標範圍很大或會是負的 → 改用模板 13(HashMap)

認出該用模板 5:

  • 題目提到:「2D matrix」、「rectangle sum」、「submatrix」
  • 需要求 (r1,c1) 到 (r2,c2) 這塊矩形的總和
  • 公式:total - left - top + topleft

認出該用模板 6:

  • 題目提到:「count odd/even」、「binary conditions」、「transform array」
  • 先轉換陣列(例如奇數→1、偶數→0),再套前綴和
  • 會化簡成更單純的前綴和問題

認出該用模板 7:

  • 題目提到:「sum of distances」、「absolute differences」、「identical elements」
  • 真正的觸發條件更廣:**在一個你可以排序的集合上求 |x - y| 的總和。**排序把絕對值拿掉, 於是每一側都是 count * pivot ∓ sum
  • 需要對相同值的元素算出 sum of |i - j|
  • 關鍵洞見:拆成左右兩半,套 count * value - sum 公式
  • HashMap 存的是:{value: [索引清單]} —— 而那些清單是免費附贈排序好的,所以不要再去排序
  • 時間複雜度從 O(n²) 降到 O(n)
  • 如果拆分點是查詢值而不是某個元素(LC 2602),先二分搜出它的序號 —— 那兩半的算法完全不變

認出該用模板 8:

  • 題目提到:「max chunks」、「切成幾段讓每段能各自排序」、「split to sort」
  • 輸入陣列是 [0, n-1] 的一個排列(或可以用前綴/後綴陣列推廣)
  • 關鍵洞見:maxSoFar == i 代表前綴 [0..i] 已經是一組完整、自成一體、可以直接排序的集合
  • 等價的檢查:arr[0..i] 的前綴和等於排序後陣列 [0..i] 的前綴和

認出該用模板 15:

  • 題目提到:「split the array」、「pivot」、「turning point」、「close the shop at hour i」、 「both robots play optimally」
  • 真正的觸發條件:**候選答案的數量是「每個索引一個」。**一個只能往下掉一次的 2 x n 格子、 把字串切成兩段、分成左右兩部分
  • 關鍵洞見:索引左邊是前綴和、右邊是後綴和 —— 掃過索引,兩側都能 O(1) 算出來
  • 注意順序:比較之前先縮後綴、比較之後才長前綴,這樣拆分點那一格才會落在兩邊之外
  • 如果目標函數是 min(max(...))max(min(...)),那就是 minimax —— 貪婪地「拿對我最好的」 是錯的目標函數,不只是弱一點而已

實作範例

八題實作放在 prefix_sum_examples.md — 都是上面的模板沒有從頭到尾解掉的:

另外五題以前有自己的範例章節,現在沒有了:LC 370、560、769、1248 和 2615 都已經被 點名它們的那個模板解掉,多出來的第二份實作並沒有補上模板缺的東西。那些副本真正 有價值的部分 — 為什麼 map 存次數而不是索引、為什麼 LC 769 光比和就夠、以及為什麼 轉換不必真的建出陣列 — 已經以註解的形式併進模板裡。

總結與速查

複雜度速查

操作 時間 空間 備註
建前綴和陣列 O(n) O(n) 一次性前處理
區間求和查詢 O(1) O(1) 前處理完之後
用 HashMap 求子陣列和 O(n) O(n) 平均情況,最壞 O(n²)
建二維前綴和 O(mn) O(mn) m×n 的矩陣
二維區間查詢 O(1) O(1) 前處理完之後
差分陣列更新 O(k) O(n) k 次更新,陣列大小 n
前綴 + 後綴拆分的掃描 O(n) O(1) 兩個累加值,不留陣列

模板速查

模板 模式 關鍵程式片段
模板 1 基本區間求和 prefix[i+1] = prefix[i] + nums[i]
模板 2 HashMap + 目標值 if prefix_sum - k in map: count += map[prefix_sum - k]
模板 3 取模/整除 remainder = prefix_sum % k; if remainder in map...
模板 4 區間更新 diff[start] += val; diff[end+1] -= val
模板 5 二維矩陣 prefix[i][j] = val + left + top - topleft
模板 6 轉換後計數 先轉換陣列,再套前綴和
模板 7 距離總和 left = idx*countLeft - sumLeft; right = sumRight - idx*countRight —— 或寫成一行,total - 2*prefix[k] + idx*(2*k - m)k = 組內序號,m = 組大小)
模板 8 前綴最大值 maxSoFar = max(maxSoFar, arr[i]); if (maxSoFar == i) chunks++
模板 9 補集(取兩端) ans = total - min(window of length n-k)
模板 10 單調雙端佇列(有負數) while p[i]-p[dq[0]]>=k: ans=min(ans,i-dq.popleft())
模板 11 列對壓縮 for top: for bot: colSum[c]+=mat[bot][c] → 再用一維解法
模板 12 前綴 XOR p[i+1] = p[i] ^ a[i]; xor(l,r) = p[r+1] ^ p[l]
模板 13 稀疏差分(HashMap) d[start]+=v; d[end+1]-=v; for k in sorted(d): cur+=d[k]
模板 15 前綴 + 後綴拆分 top-=a[i]; res=min(res,max(top,bottom)); bottom+=b[i]

模板 9–13 的完整內容寫在 prefix_sum_advanced.md

核心數學洞見

前綴和公式

python
# For 1D array: sum of subarray [i, j] (inclusive)
subarray_sum = prefix[j + 1] - prefix[i]

# For 2D matrix: sum of rectangle from (r1,c1) to (r2,c2)
rectangle_sum = prefix[r2+1][c2+1] - prefix[r1][c2+1] - prefix[r2+1][c1] + prefix[r1][c1]

HashMap 的關鍵洞見

python
# If prefix_sum[j] - prefix_sum[i] = k
# Then prefix_sum[i] = prefix_sum[j] - k
# So check if (current_prefix_sum - k) exists in map

# For divisibility: if (sum[j] - sum[i]) % k = 0
# Then sum[j] % k = sum[i] % k
# So check if (current_sum % k) exists in remainder map

常見模式與技巧

模式 1:Two Sum 的延伸

python
# Convert "find subarray with sum = k" to "find two prefix sums with diff = k"
def subarray_sum_equals_k(nums, k):
    prefix_sum = 0
    count = 0
    prefix_map = {0: 1}  # Critical: handle subarrays from index 0
    
    for num in nums:
        prefix_sum += num
        count += prefix_map.get(prefix_sum - k, 0)
        prefix_map[prefix_sum] = prefix_map.get(prefix_sum, 0) + 1
    
    return count

模式 2:差分陣列的魔法

python
# Apply multiple range updates [start, end, val] efficiently
def range_addition(length, updates):
    diff = [0] * (length + 1)  # Extra space for end+1 indexing
    
    for start, end, val in updates:
        diff[start] += val      # Mark start
        diff[end + 1] -= val    # Mark end+1 (undo effect)
    
    # Convert difference array to result using prefix sum
    result = []
    current = 0
    for i in range(length):
        current += diff[i]      # This is prefix sum computation!
        result.append(current)
    
    return result

模式 3:先轉換再求和

python
# Many problems can be reduced to simpler prefix sum problems
def count_nice_subarrays(nums, k):
    # Transform: odd numbers → 1, even numbers → 0
    # Problem becomes: count subarrays with sum = k
    binary_array = [1 if x % 2 == 1 else 0 for x in nums]
    return subarray_sum_equals_k(binary_array, k)

解題步驟

  1. 先認出模式

    • 仔細讀題,抓關鍵字(range、subarray、sum、count 等等)
    • 確認是多次查詢還是單趟掃描
    • 找找有沒有數學關係(整除、取模等等)
  2. 挑對模板

    • 用決策流程圖選出合適的模板
    • 考慮時間/空間複雜度的要求
    • 確認套前綴和之前需不需要先做轉換
  3. 處理邊界情況

    • 空陣列或只有一個元素
    • 負數(取模運算時尤其要小心)
    • 大數相加時的整數溢位
    • 值為 0 的元素對整除判斷的影響
  4. 最佳化實作

    • HashMap 要先放基底情況(通常是 {0: 1}
    • 取模時處理負餘數
    • 能寫成單趟就寫成單趟
    • 只需要次數時,考慮省掉空間

常見錯誤與提示

🚫 常見錯誤:

  • 忘了基底情況:子陣列題沒有先把 HashMap 初始化成 {0: 1}
  • 差一錯誤:前綴和陣列的索引算錯
  • 負餘數:取模時沒處理 remainder < 0
  • HashMap 的時機:先寫進 map 還是先檢查條件,順序搞反
  • 二維索引:二維前綴和裡列跟行搞混
  • 區間更新:差分陣列忘了在 end+1 減回去

✅ 最佳實務:

  • 前綴和陣列一律n+1 大小,用 1-based 索引
  • 子陣列題一律先在 HashMap 放 {0: 1},處理掉邊界情況
  • 再三確認順序:先檢查條件,再更新 HashMap
  • 處理負數:取模用 remainder = (remainder % k + k) % k
  • 檢查邊界:用到 end+1 索引時記得檢查陣列範圍
  • 測邊界情況:空陣列、單一元素、全負數

面試提示

  1. 模式辨識

    • 看到「subarray sum equals K」→ 直接 HashMap + 前綴和
    • 看到「range queries」→ 基本前綴和陣列
    • 看到「divisible by K」→ HashMap 搭配取模技巧
    • 看到「多次區間更新」→ 差分陣列
    • 如果整個選擇就是一個索引(一刀、一個 pivot、一個轉彎欄)→ 前綴 + 後綴的掃描
  2. 表達策略

    • 把數學洞見講出來:「我們是在找兩個前綴和的配對」
    • 畫例子示範前綴和怎麼運作
    • 主動提複雜度的改善:「這把 O(n²) 降到 O(n)」
    • 討論時間與空間的取捨
  3. 實作技巧

    • 先寫暴力解確認自己理解對了
    • 再用合適的前綴和模板去最佳化
    • 解釋為什麼 HashMap 的初始化很重要
    • 拿一個小例子一步一步走過去
  4. 常見追問

    • 討論變形:「如果要的是最大長度而不是數量呢?」
    • 說明推廣到二維:「在矩陣上該怎麼做?」
    • 考慮限制條件:「如果數字非常大呢?」(溢位)

相關主題

  • HashMap/雜湊表:大多數進階前綴和題目的必需品
  • 滑動視窗:可以跟前綴和結合起來最佳化
  • Two Sum:很多前綴和題目其實是 two sum 的延伸
  • 動態規劃:前綴和常拿來當 DP 的最佳化手段
  • 二分搜尋:可以跟前綴和結合做區間查詢
  • 線段樹:需要邊更新邊查詢區間和時的替代方案
  • 單調堆疊:有時會跟前綴和一起用來最佳化
  • 樹的 DFS:一條 root→node 的鏈就是一個陣列,所以模板 2 可以用來數往下的路徑 —— 模板 14,LC 437

進階延伸

  • 稀疏陣列:座標壓縮搭配前綴和
  • 線上查詢:需要更新 + 查詢時改用線段樹或樹狀陣列
  • 二維區間更新:二維差分陣列搭配二維前綴和
  • 帶權前綴和:處理各元素權重不同的情況
  • 環狀陣列:改寫模板來處理繞回頭的情況

這份 cheatsheet 涵蓋了所有主要的前綴和模式,並提供一套有系統的方法,讓你能高效率地解掉 40 多題 LeetCode。