Prefix Sum (前綴和)
範圍 — 前綴和/累積和 — 子陣列和、二維前綴和、前綴和搭配雜湊表做計數。 另見: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)
- 二維區間求和查詢
參考資料
- Fucking Algorithm - Prefix Sum
- LeetCode Prefix Sum Problems
- LeetCode Problem Set Discussion
- Hash Map Cheatsheet
題型分類
模式 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) 概念
前綴和陣列怎麼建(核心)
整套技巧都建立在一行核心程式上。把它背下來,其他都是推論:
for i in range(len(cnt)):
prefix[i + 1] = prefix[i] + cnt[i]
一步一步來:
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):
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
重點:
prefix比cnt多一個元素。prefix[i+1]回答的是 「前i+1個元素的和」=cnt[0] + ... + cnt[i]。
一行版寫法(itertools.accumulate 前面補一個 0):
from itertools import accumulate
prefix = list(accumulate(cnt, initial=0)) # [0, 1, 1, 2, 3, 4]
為什麼 sum(l, r) = prefix[r+1] - prefix[l]

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 這個邊界情況:
sum(0, 2) = prefix[3] - prefix[0]
= (a + b + c) - 0
= a + b + c ✓
沒有它的話,就得特別寫 if (left == 0) 的判斷(見 LC 303 的 V0)。
具體範例 — LC 303
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 | 兩個累加值 | 整個答案由單一個索引決定 |
通用模板
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
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 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
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 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
核心數學洞見:
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.
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
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
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
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| 的總和。
核心想法
兩個動作,順序如下:
- 先把索引依值分組(
{value: [indices]}),因為|i - j|只會發生在相同的值之間 —— 每一組都是彼此獨立、自成一題的子問題。 - **在一組之內,靠拆分點把絕對值拿掉。**拆分點左邊全是
pivot - other,右邊全是other - pivot;絕對值符號一個都不剩,而每一半都是count * pivot ∓ sum—— 差一個前綴和而已。
**每一組的索引是免費附贈排序好的。**你是從左到右掃描時把
i一個個 append 進去的,所以 每個清單本來就遞增。這正是整個做法能是O(n)的原因 —— 去排序那些組要花O(n log n), 而且什麼也換不到。這裡千萬不要排序;一排序就露出了「沒搞懂分組這一步」。
推導
先講記號 —— 本節後面都照這套用
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是組內的序號 —— 它索引的是indices與prefix。idx = indices[k]才是在nums裡的位置 —— 它是被加總的那個值, 也是答案要寫回去的地方(res[idx],絕對不是res[k])。
怎麼讀一個前綴索引 —— 整段推導就靠這一條規則
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]:
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,絕對值符號就掉了:
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:
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) 總距離
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)。
圖解
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]:
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 模板
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 模板
// 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;
}
替代作法:邊走邊累加(不建前綴陣列)
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:
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
# 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 = k、sum_left = prefix[k] |
idx 之前那 k 個相同的值 |
| 右側個數/總和 | right_cnt = m - 1 - k、sum_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 + right → res[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 次:個數變成權重和,所以要對 w 與 w*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 — 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 — 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)):
// 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,一般陣列):
// 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 一樣留著
prefixMax與suffixMin,但它檢查的是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 條,不是指數多條。
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:
answer = min over i of max( sum(row0[i+1 ... n-1]) , sum(row1[0 ... i-1]) )
└──── suffix, shrinks ─┘ └──── prefix, grows ─┘
模式
兩個累加值往相反方向移動,而迴圈裡那三行的順序就是整個模板:
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
# 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
// 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]]
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 | 帶權重 —— 對 w 與 w*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 + 前綴 | 進階 |
模式選擇策略
前綴和題目的決策框架
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 — 都是上面的模板沒有從頭到尾解掉的:
| 分組 | 題目 |
|---|---|
| 用 HashMap 求子陣列和 | LC 325, 523, 1124, 926 |
| 固定視窗與成對視窗 | LC 1031 |
| 二維前綴和 | LC 1292 |
| 區間更新 | LC 1094 |
| 前綴最大值/後綴最小值的掃描 | LC 2012 |
另外五題以前有自己的範例章節,現在沒有了: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。
核心數學洞見
前綴和公式
# 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 的關鍵洞見
# 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 的延伸
# 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:差分陣列的魔法
# 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:先轉換再求和
# 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)
解題步驟
-
先認出模式
- 仔細讀題,抓關鍵字(range、subarray、sum、count 等等)
- 確認是多次查詢還是單趟掃描
- 找找有沒有數學關係(整除、取模等等)
-
挑對模板
- 用決策流程圖選出合適的模板
- 考慮時間/空間複雜度的要求
- 確認套前綴和之前需不需要先做轉換
-
處理邊界情況
- 空陣列或只有一個元素
- 負數(取模運算時尤其要小心)
- 大數相加時的整數溢位
- 值為 0 的元素對整除判斷的影響
-
最佳化實作
- HashMap 要先放基底情況(通常是
{0: 1}) - 取模時處理負餘數
- 能寫成單趟就寫成單趟
- 只需要次數時,考慮省掉空間
- HashMap 要先放基底情況(通常是
常見錯誤與提示
🚫 常見錯誤:
- 忘了基底情況:子陣列題沒有先把 HashMap 初始化成
{0: 1} - 差一錯誤:前綴和陣列的索引算錯
- 負餘數:取模時沒處理
remainder < 0 - HashMap 的時機:先寫進 map 還是先檢查條件,順序搞反
- 二維索引:二維前綴和裡列跟行搞混
- 區間更新:差分陣列忘了在
end+1減回去
✅ 最佳實務:
- 前綴和陣列一律開
n+1大小,用 1-based 索引 - 子陣列題一律先在 HashMap 放
{0: 1},處理掉邊界情況 - 再三確認順序:先檢查條件,再更新 HashMap
- 處理負數:取模用
remainder = (remainder % k + k) % k - 檢查邊界:用到
end+1索引時記得檢查陣列範圍 - 測邊界情況:空陣列、單一元素、全負數
面試提示
-
模式辨識
- 看到「subarray sum equals K」→ 直接 HashMap + 前綴和
- 看到「range queries」→ 基本前綴和陣列
- 看到「divisible by K」→ HashMap 搭配取模技巧
- 看到「多次區間更新」→ 差分陣列
- 如果整個選擇就是一個索引(一刀、一個 pivot、一個轉彎欄)→ 前綴 + 後綴的掃描
-
表達策略
- 把數學洞見講出來:「我們是在找兩個前綴和的配對」
- 畫例子示範前綴和怎麼運作
- 主動提複雜度的改善:「這把 O(n²) 降到 O(n)」
- 討論時間與空間的取捨
-
實作技巧
- 先寫暴力解確認自己理解對了
- 再用合適的前綴和模板去最佳化
- 解釋為什麼 HashMap 的初始化很重要
- 拿一個小例子一步一步走過去
-
常見追問
- 討論變形:「如果要的是最大長度而不是數量呢?」
- 說明推廣到二維:「在矩陣上該怎麼做?」
- 考慮限制條件:「如果數字非常大呢?」(溢位)
相關主題
- HashMap/雜湊表:大多數進階前綴和題目的必需品
- 滑動視窗:可以跟前綴和結合起來最佳化
- Two Sum:很多前綴和題目其實是 two sum 的延伸
- 動態規劃:前綴和常拿來當 DP 的最佳化手段
- 二分搜尋:可以跟前綴和結合做區間查詢
- 線段樹:需要邊更新邊查詢區間和時的替代方案
- 單調堆疊:有時會跟前綴和一起用來最佳化
- 樹的 DFS:一條 root→node 的鏈就是一個陣列,所以模板 2 可以用來數往下的路徑 —— 模板 14,LC 437
進階延伸
- 稀疏陣列:座標壓縮搭配前綴和
- 線上查詢:需要更新 + 查詢時改用線段樹或樹狀陣列
- 二維區間更新:二維差分陣列搭配二維前綴和
- 帶權前綴和:處理各元素權重不同的情況
- 環狀陣列:改寫模板來處理繞回頭的情況
這份 cheatsheet 涵蓋了所有主要的前綴和模式,並提供一套有系統的方法,讓你能高效率地解掉 40 多題 LeetCode。