堆積(Heap) — 實戰題解
範圍 — 堆積/優先佇列(priority queue)家族的題目集:每題都歸到它所化約成的 heap.md 模板之下,並寫出它額外多出的那一步;模板本身,以及需要進階技巧的題目,放在相鄰的文件裡。 另見 — 主文件:heap.md — 這些範例所實作的標準模板與模式選擇指南。從同一份檔案拆出去的鄰近文件:heap_advanced.md — 延遲刪除(lazy deletion)、反悔貪婪(regret greedy)、掃描線,以及較少見的堆積模板;heap_language_apis.md — 完整的
heapq/PriorityQueueAPI 參考。
LeetCode 題目清單
總覽
heap.md 放的是八個堆積模板;這份文件放的是題目。下面每一節對應一個模板,順序相同, 每張卡片點出這題在模板之上多出的那一步。解法需要核心模板沒有的技巧(延遲刪除、反悔、 範圍跳躍的格子)的題目,詳解放在 heap_advanced.md;別份文件已經解過的題目, 這裡只留一列指標,絕不再寫第二份。
索引
| LC | 題目 | 化約到 | 難度 | 位置 |
|---|---|---|---|---|
| LC 1046 | Last Stone Weight | heap § 模板 1 | 簡單 | 卡片 |
| LC 703 | Kth Largest Element in a Stream | heap § 模板 1 | 簡單 | 卡片 |
| LC 1167 | Minimum Cost to Connect Sticks | heap § 模板 1 | 中等 | 卡片 |
| LC 1492 | The kth Factor of n | heap § 模板 1 | 中等 | 卡片 |
| LC 973 | K Closest Points to Origin | heap § 模板 1 | 中等 | 卡片 |
| LC 1383 | Maximum Performance of a Team | heap_advanced § 6 | 困難 | 詳解在 heap_advanced.md |
| LC 347 | Top K Frequent Elements | heap § 模板 2 | 中等 | 詳解在 heap.md |
| LC 451 | Sort Characters By Frequency | heap § 模板 2 | 中等 | 卡片 |
| LC 1481 | Least Number of Unique Integers after K Removals | heap § 模板 2 | 中等 | 卡片 |
| LC 1647 | Minimum Deletions to Make Character Frequencies Unique | heap § 模板 2 | 中等 | 卡片 |
| LC 3092 | Most Frequent IDs | heap_advanced § 1 | 中等 | 詳解在 heap_advanced.md |
| LC 23 | Merge k Sorted Lists | heap § 模板 3 | 困難 | 詳解在 heap.md |
| LC 264 | Ugly Number II | Dijkstra_examples § Variation B | 中等 | 詳解在 Dijkstra_examples.md |
| LC 373 | Find K Pairs with Smallest Sums | heap § 模板 3 | 中等 | 卡片 |
| LC 378 | Kth Smallest Element in a Sorted Matrix | heap § 模板 3 | 中等 | 卡片 |
| LC 1675 | Minimize Deviation in Array | heap § 模板 3 | 困難 | 卡片 |
| LC 295 | Find Median from Data Stream | heap § 模板 4 | 困難 | 詳解在 heap.md |
| LC 480 | Sliding Window Median | monotonic_queue § 模板 4 | 困難 | 詳解在 monotonic_queue.md |
| LC 1353 | Maximum Number of Events That Can Be Attended | scanning_line_examples § LC 1353 | 中等 | 詳解在 scanning_line_examples.md |
| LC 2406 | Divide Intervals Into Minimum Number of Groups | scanning_line_examples § LC 2406 | 中等 | 詳解在 scanning_line_examples.md |
| LC 767 | Reorganize String | heap § 模板 7 | 中等 | 卡片 |
| LC 621 | Task Scheduler | heap § 模板 8 | 中等 | 卡片 |
| LC 871 | Minimum Number of Refueling Stops | heap_advanced § 4 | 困難 | 詳解在 heap_advanced.md |
| LC 2617 | Minimum Number of Visited Cells in a Grid | heap_advanced § 8 | 困難 | 詳解在 heap_advanced.md |
| LC 895 | Maximum Frequency Stack | design_examples § LC 895 | 困難 | 詳解在 design_examples.md — 頻率分桶,不需要堆積 |
大小為 k 的堆積範例
Last Stone Weight — LC 1046
關鍵 — 這裡的堆積沒有上限 k:它就是整個池子,每一步 pop 出最重的兩顆,再把差值 push 回去。最重的兩顆永遠是最先 pop 出來的兩個,所以每次相撞是 O(log n)。
歸約到:heap § 模板 1 — max-heap 是整份輸入而不是最好的 k 個,每次 pop 兩個。
# python
# LC 1046 - Last Stone Weight
# IDEA: max-heap (negated) of every stone; smash the top two, push back the difference
# time = O(n log n), space = O(n)
import heapq
def lastStoneWeight(stones):
heap = [-s for s in stones] # negated: heaviest on top
heapq.heapify(heap)
while len(heap) > 1:
first = -heapq.heappop(heap) # heaviest
second = -heapq.heappop(heap) # second heaviest
if first != second:
heapq.heappush(heap, -(first - second))
return -heap[0] if heap else 0
// java
// LC 1046 - Last Stone Weight
// IDEA: max-heap of every stone; smash the top two, offer back the difference
// time = O(n log n), space = O(n)
public int lastStoneWeight(int[] stones) {
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
for (int stone : stones) {
maxHeap.offer(stone);
}
while (maxHeap.size() > 1) {
int first = maxHeap.poll(); // heaviest
int second = maxHeap.poll(); // second heaviest
if (first != second) {
maxHeap.offer(first - second); // the survivor goes back into the pool
}
}
return maxHeap.isEmpty() ? 0 : maxHeap.peek();
}
陷阱
[2,2]會把兩顆都撞毀:堆積最後是空的,所以要先檢查是否為空再讀heap[0],並回傳 0。- 兩顆一樣重時還 push 一個 0,會留下一顆之後「免費」相撞的
0石頭 — 不影響答案, 但是白做一次 push;直接略過。
Kth Largest Element in a Stream — LC 703 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
關鍵 — 大小為 k 的 min-heap 會跨呼叫保留:
add先 push,超過 k 就驅逐最小的, 再回傳堆頂。建構物件就是對初始陣列逐一呼叫有上限的add,而初始陣列可能少於 k 個數。
歸約到:heap § 模板 1 — 同一個堆積,改成欄位,讓它在查詢之間保留下來。
# python
# LC 703 - Kth Largest Element in a Stream
# IDEA: a min-heap of the k largest seen so far, kept between calls; its top is the kth largest
# time = O(log k) per add (O(n log k) to build), space = O(k)
import heapq
class KthLargest:
def __init__(self, k, nums):
self.k = k
self.heap = [] # the k largest seen so far
for num in nums:
self.add(num)
def add(self, val):
heapq.heappush(self.heap, val)
if len(self.heap) > self.k:
heapq.heappop(self.heap) # evict the smallest; the k largest remain
return self.heap[0] # smallest of the k largest = kth largest
// java
// LC 703 - Kth Largest Element in a Stream
// IDEA: a min-heap of the k largest seen so far, kept between calls; its top is the kth largest
// time = O(log k) per add, space = O(k)
class KthLargest {
private final PriorityQueue<Integer> minHeap = new PriorityQueue<>(); // the k largest
private final int k;
public KthLargest(int k, int[] nums) {
this.k = k;
for (int num : nums) {
add(num);
}
}
public int add(int val) {
minHeap.offer(val);
if (minHeap.size() > k) {
minHeap.poll(); // evict the smallest; the k largest remain
}
return minHeap.peek();
}
}
陷阱
nums可能比 k 短(KthLargest(1, [])):建構子裡絕不能假設堆積已滿 — 一律經過add就同時處理兩種情況。- 先 heapify
nums再 pop 到剩 k 個,會改動呼叫端的串列;請自己建一個堆積。
類似 — 順序統計 BST 對任意名次都能回答同樣的查詢: bst_advanced.md。
Minimum Cost to Connect Sticks — LC 1167
關鍵 — 永遠合併最短的兩根:一根棍子的長度在它參與的每次合併都要付一次, 所以最短的應該放在合併樹的最深處(Huffman 編碼)。合併後的棍子放回池子裡。
歸約到:heap § 模板 1 — 沒有上限、裝著整個池子的 min-heap,像 LC 1046 一樣每次 pop 兩個。
# python
# LC 1167 - Minimum Cost to Connect Sticks
# IDEA: Huffman merge - pop the two shortest, pay their sum, push the merged stick back
# time = O(n log n), space = O(n)
import heapq
def connectSticks(sticks):
heap = list(sticks)
heapq.heapify(heap)
cost = 0
while len(heap) > 1:
a = heapq.heappop(heap) # the two shortest sticks
b = heapq.heappop(heap)
cost += a + b
heapq.heappush(heap, a + b) # the merged stick goes back into the pool
return cost
陷阱
- 只有一根棍子時成本是 0:迴圈不能執行,所以條件是
len(heap) > 1。 - 排序一次再由左往右合併是錯的:
[1,8,3,5]先合併 1+3 = 4,接著是 4+5,不是 4+8 — 合併後的棍子必須重新和其他棍子競爭。
The kth Factor of n — LC 1492
關鍵 — 因數成對出現
(i, n // i),且i ≤ √n,所以 √n 次試除就能找到全部。 用大小為 k 的 max-heap 存看過的因數,留住最小的 k 個;只要 n 至少有 k 個因數, 堆頂就是第 k 小。
歸約到:heap § 模板 1 — 鏡像成第 k 小,對象是成對產生的因數。
# python
# LC 1492 - The kth Factor of n
# IDEA: enumerate divisor pairs up to sqrt(n); a size-k max-heap keeps the k smallest
# time = O(sqrt(n) log k), space = O(k)
import heapq
def kthFactor(n, k):
heap = [] # max-heap (negated) of the k smallest divisors
def push(d):
heapq.heappush(heap, -d)
if len(heap) > k:
heapq.heappop(heap) # drop the largest; the k smallest remain
i = 1
while i * i <= n:
if n % i == 0:
push(i)
if i != n // i:
push(n // i) # the paired divisor
i += 1
return -heap[0] if len(heap) == k else -1
陷阱
- 完全平方數的平方根只算一次:
n = 16→4不能 push 兩次,否則 k = 3 會因為錯的理由 回傳 4,k = 4 則差一。 - 因數少於 k 個(
n = 4, k = 4)要回傳 -1;檢查堆積大小,而不只是是否為空。 - 不用堆積的話,小因數本來就依序出現、大因數則反序出現 — 用兩個串列就是 O(√n) 時間。 被問到更好的解法時就說這個。
K Closest Points to Origin — LC 973 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
關鍵 — 「最近的 k 個」就是「依距離最小的 k 個」:用大小為 k、以距離平方為 key 的 max-heap,留下的 k 個裡最遠的在堆頂,被驅逐的就是它。
歸約到:heap § 模板 1 — 鏡像成最小的 k 個,key 由點推導而來。
# python
# LC 973 - K Closest Points to Origin
# IDEA: size-k max-heap (negated) by squared distance; evict the farthest when it passes k
# time = O(n log k), space = O(k)
import heapq
def kClosest(points, k):
heap = [] # max-heap (negated distance) of the k closest
for x, y in points:
dist = x * x + y * y # squared distance: no sqrt, no float
heapq.heappush(heap, (-dist, x, y))
if len(heap) > k:
heapq.heappop(heap) # evict the farthest of the k + 1
return [[x, y] for _, x, y in heap]
// java
// LC 973 - K Closest Points to Origin
// IDEA: size-k max-heap by squared distance; evict the farthest when it passes k
// time = O(n log k), space = O(k)
public int[][] kClosest(int[][] points, int k) {
// max-heap by squared distance: the farthest of the k kept is on top
PriorityQueue<int[]> maxHeap = new PriorityQueue<>(
(a, b) -> Integer.compare(b[0] * b[0] + b[1] * b[1], a[0] * a[0] + a[1] * a[1]));
for (int[] point : points) {
maxHeap.offer(point);
if (maxHeap.size() > k) {
maxHeap.poll(); // evict the farthest
}
}
int[][] result = new int[k][];
for (int i = 0; i < k; i++) {
result[i] = maxHeap.poll();
}
return result;
}
陷阱
- 把全部 n 個點放進 min-heap 再 pop k 次是 O(n log n)、O(n) 空間 — 上限才是這個模板的重點。
- 比較
Math.sqrt距離只會平白多出浮點誤差;距離平方的大小順序完全一樣。
類似 — 單次查詢、靜態資料時,O(n) 平均的答案是 quickselect: quickselect § 模板 2。 點是串流進來、或記憶體必須維持 O(k) 時,堆積勝出。
| LC | 題目 | 所在 | 為什麼不在這裡 |
|---|---|---|---|
| LC 1383 | Maximum Performance of a Team | heap_advanced § 6 | 大小為 k 的堆積是跑在依另一個屬性排序的迴圈裡 |
先計數再用堆積的範例
| LC | 題目 | 所在 | 說明 |
|---|---|---|---|
| LC 347 | Top K Frequent Elements | heap § 模板 2 | 錨點題,旁邊附 O(n) 的桶排序版本 |
| LC 3092 | Most Frequent IDs | heap_advanced § 1 | 計數在 push 之後還會變 → 延遲刪除 |
Sort Characters By Frequency — LC 451
關鍵 — 計數一旦確定,堆積就是整段整段地輸出:pop 出最頻繁的字元,把它附加
count次。這裡沒有 k,所以堆積沒有上限,一路清空。
歸約到:heap § 模板 2 — 先計數,再依計數排序;k 就是相異字元的數量。
# python
# LC 451 - Sort Characters By Frequency
# IDEA: count, then a max-heap by count; pop each character and emit its whole run
# time = O(n + d log d), space = O(n) d = distinct characters
import heapq
from collections import Counter
def frequencySort(s):
count = Counter(s)
heap = [(-freq, ch) for ch, freq in count.items()] # most frequent on top
heapq.heapify(heap)
parts = []
while heap:
neg_freq, ch = heapq.heappop(heap)
parts.append(ch * -neg_freq) # emit the whole run at once
return "".join(parts)
// java
// LC 451 - Sort Characters By Frequency
// IDEA: count, then a max-heap of characters by count; pop each and emit its whole run
// time = O(n + d log d), space = O(n) d = distinct characters
public String frequencySort(String s) {
Map<Character, Integer> freq = new HashMap<>();
for (char c : s.toCharArray()) {
freq.put(c, freq.getOrDefault(c, 0) + 1);
}
PriorityQueue<Character> maxHeap = new PriorityQueue<>(
(a, b) -> Integer.compare(freq.get(b), freq.get(a))); // most frequent on top
maxHeap.addAll(freq.keySet());
StringBuilder sb = new StringBuilder();
while (!maxHeap.isEmpty()) {
char c = maxHeap.poll();
for (int i = 0; i < freq.get(c); i++) {
sb.append(c); // emit the whole run at once
}
}
return sb.toString();
}
陷阱
- 大寫和小寫是不同字元:
"Aabb"→"bbAa"或"bbaA",絕不會是"bbaa"。 - 在 Java 的迴圈裡用
+=組字串是 O(n²);請用StringBuilder。 - 計數最多是 n,所以依計數做桶排序是 O(n) — 和 LC 347 的第二種解法是同一招,見 heap § 模板 2。
Least Number of Unique Integers after K Removals — LC 1481
關鍵 — 要讓剩下的相異值最少,就先把計數最小的值整個清掉:計數的 min-heap, 只要整個計數還放得進 k 就 pop。只放得進一部分的值不值得碰。
歸約到:heap § 模板 2 — 先計數,再依計數 pop;停止條件是預算 k,而不是大小。
# python
# LC 1481 - Least Number of Unique Integers after K Removals
# IDEA: min-heap of counts; remove whole values, cheapest first, while k covers them
# time = O(n + d log d), space = O(d) d = distinct values
import heapq
from collections import Counter
def findLeastNumOfUniqueInts(arr, k):
heap = list(Counter(arr).values()) # min-heap of counts: cheapest to wipe out on top
heapq.heapify(heap)
while heap and heap[0] <= k:
k -= heapq.heappop(heap) # remove every copy of that value
return len(heap)
陷阱
- 一次只減一個複本(k 次 pop/push)是 O(k log d),雖然會過,但把移除次數花在永遠刪不完的值上; 請整個計數一起 pop。
heap and要放前面:[1]、k = 1 會把堆積清空,答案是 0。
Minimum Deletions to Make Character Frequencies Unique — LC 1647
關鍵 — 用 max-heap 從最大的計數往下處理:堆頂等於下一個計數時,就削掉一個再 push 回去。一個計數只有在嚴格大於堆積裡所有計數時才會離開堆積,所以離開的計數全都不同。
歸約到:heap § 模板 2 — 先計數,再對計數用 max-heap;多出的一步是平手時減一再放回。
# python
# LC 1647 - Minimum Deletions to Make Character Frequencies Unique
# IDEA: max-heap of counts; on a tie with the next count, delete one and push it back
# time = O(n + K log K), space = O(K) K = distinct letters (26), each count shaved at most n times
import heapq
from collections import Counter
def minDeletions(s):
heap = [-f for f in Counter(s).values()] # max-heap of counts
heapq.heapify(heap)
deletions = 0
while len(heap) > 1:
top = -heapq.heappop(heap)
if top == -heap[0]: # the next count is the same: shave one off this one
deletions += 1
if top - 1 > 0:
heapq.heappush(heap, -(top - 1))
# otherwise `top` is now unique and leaves the heap for good
return deletions
// java
// LC 1647 - Minimum Deletions to Make Character Frequencies Unique
// IDEA: max-heap of counts; on a tie with the next count, delete one and offer it back
// time = O(n + K log K), space = O(K) K = distinct letters (26)
public int minDeletions(String s) {
int[] freq = new int[26];
for (char c : s.toCharArray()) {
freq[c - 'a']++;
}
PriorityQueue<Integer> pq = new PriorityQueue<>(Collections.reverseOrder()); // max-heap of counts
for (int f : freq) {
if (f > 0) {
pq.add(f);
}
}
int deletions = 0;
while (pq.size() > 1) {
int top = pq.poll();
if (top == pq.peek()) { // tied with the next count: shave one off
deletions++;
if (top - 1 > 0) {
pq.add(top - 1);
}
} // otherwise top is unique and leaves for good
}
return deletions;
}
陷阱
- 被削到 0 的計數代表那個字母已整個刪除;不要再 push 回去,否則兩個 0 會「撞在一起」
又被多算一次(
"abcabc"是 3,不會更多)。 - 用排序的版本要小心:某個計數被歸零之後,它後面的計數仍可能相撞
(
"aebacabbbad"→ 4,4,1,1,1 需要刪 3 次)。一遇到 0 就跳出迴圈會回傳 2。 - 另一個常見答案是用
set記錄用過的計數(一直減到沒用過為止);它是同一個貪婪,只是不排序。
K 路合併範例
| LC | 題目 | 所在 | 說明 |
|---|---|---|---|
| LC 23 | Merge k Sorted Lists | heap § 模板 3 | 錨點題,旁邊附兩兩分治合併 |
| LC 264 | Ugly Number II | Dijkstra_examples § Variation B | 堆積 + seen 集合,展開 v·2, v·3, v·5 — 最佳優先的生成 |
Find K Pairs with Smallest Sums — LC 373
關鍵 — 虛擬格子的第 i 列
(i, 0), (i, 1), …本來就依和排序,所以這些列就是 k 個有序串列。把每列的頭(i, 0)放進去,pop 時只讓那一列前進到(i, j + 1)— 每個配對只被 push 一次,所以不需要seen集合。
歸約到:heap § 模板 3 — 來源是一個從未真正建出來的格子的各列,而且只有前 k 列可能有用。
# python
# LC 373 - Find K Pairs with Smallest Sums
# IDEA: k-way merge over rows: seed (i, 0) for the first k rows, pop k times, advance the popped row
# time = O(k log k), space = O(k)
import heapq
def kSmallestPairs(nums1, nums2, k):
# one source per row i: the pairs (i, 0), (i, 1), ... are sorted by sum
heap = [(nums1[i] + nums2[0], i, 0) for i in range(min(k, len(nums1)))]
heapq.heapify(heap)
res = []
while heap and len(res) < k:
_, i, j = heapq.heappop(heap)
res.append([nums1[i], nums2[j]])
if j + 1 < len(nums2):
heapq.heappush(heap, (nums1[i] + nums2[j + 1], i, j + 1)) # advance row i
return res
// java
// LC 373 - Find K Pairs with Smallest Sums
// IDEA: k-way merge over rows: seed (i, 0) for the first k rows, poll k times, advance the polled row
// time = O(k log k), space = O(k)
public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
List<List<Integer>> res = new ArrayList<>();
// {sum, i, j}: one entry per row i, the row's current head (i, j)
PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
for (int i = 0; i < nums1.length && i < k; i++) {
minHeap.offer(new int[]{nums1[i] + nums2[0], i, 0}); // (i, 0) is row i's smallest
}
while (res.size() < k && !minHeap.isEmpty()) {
int[] cur = minHeap.poll();
int i = cur[1], j = cur[2];
res.add(Arrays.asList(nums1[i], nums2[j]));
if (j + 1 < nums2.length) {
minHeap.offer(new int[]{nums1[i] + nums2[j + 1], i, j + 1}); // advance row i
}
}
return res;
}
陷阱
- 把每一列都放進去是白花 O(n₁ log n₁):第 i ≥ k 列的配對前面至少有 k 個更小的配對,
所以只放
min(k, len(nums1))列。 - k 可能超過
len(nums1) · len(nums2)([1,2]、[3]、k = 3 → 兩組配對):堆積空了就停。 - 每次 pop 都同時展開
(i + 1, j)和(i, j + 1)是最佳優先的變形,那個就需要seen集合 — 見 Dijkstra_examples § 11。
Kth Smallest Element in a Sorted Matrix — LC 378 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
關鍵 — 每一列都是有序來源;每列放一個頭來合併,第 k 次 pop 就是答案。 只有前
min(k, n)列可能含有最小的 k 個值之一。
歸約到:heap § 模板 3 — 各列就是 k 個有序串列,合併在第 k 個值時停止。
# python
# LC 378 - Kth Smallest Element in a Sorted Matrix
# IDEA: k-way merge of the rows; pop k - 1 values, the top is then the kth smallest
# time = O(k log n), space = O(n)
import heapq
def kthSmallest(matrix, k):
n = len(matrix)
heap = [(matrix[r][0], r, 0) for r in range(min(k, n))] # head of each row
heapq.heapify(heap)
for _ in range(k - 1): # pop k - 1 values; the next top is the kth
_, r, c = heapq.heappop(heap)
if c + 1 < n:
heapq.heappush(heap, (matrix[r][c + 1], r, c + 1))
return heap[0][0]
// java
// LC 378 - Kth Smallest Element in a Sorted Matrix
// IDEA: k-way merge of the rows; poll k - 1 values, the top is then the kth smallest
// time = O(k log n), space = O(n)
public int kthSmallest(int[][] matrix, int k) {
int n = matrix.length;
// {value, row, col}: the current head of each row
PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
for (int r = 0; r < n && r < k; r++) {
minHeap.offer(new int[]{matrix[r][0], r, 0});
}
for (int i = 0; i < k - 1; i++) { // poll k - 1 values; the next top is the kth
int[] cur = minHeap.poll();
int r = cur[1], c = cur[2];
if (c + 1 < n) {
minHeap.offer(new int[]{matrix[r][c + 1], r, c + 1});
}
}
return minHeap.peek()[0];
}
陷阱
- 對全部 n² 個格子用大小為 k 的 max-heap 也行,但它沒用到各列已排序: O(n² log k)。寫哪一個之前先說明為什麼合併比較好。
- 重複值要分開算:
[[1,2],[1,3]]、k = 2 是 1。 - k 接近 n² 時,對值域做二分搜尋是 O(n log(max − min)),勝過堆積: binary_search_on_answer § 模板 4。
Minimize Deviation in Array — LC 1675
關鍵 — 先把每個奇數提高到它能到達的最大值(× 2);從此以後值只能減半。 於是每個數都是一個往下走的值來源,偏差就是目前各個頭的
max − min:永遠把目前的 最大值減半、追蹤一路以來的最小值,最大值變成奇數時就停 — 那個來源已經用完。
歸約到:heap § 模板 3 — 往下跑的 k 路合併(每個數一個頭,最大值在頂),也就是 LC 632 涵蓋所有串列的最小範圍的形狀。
# python
# LC 1675 - Minimize Deviation in Array
# IDEA: normalise odds up (x2); max-heap of current values; halve the max while it is even,
# tracking the running min; the best max - min seen is the answer
# time = O(n log n log M), space = O(n) M = the largest value
import heapq
def minimumDeviation(nums):
heap = [] # max-heap (negated) of each number's current value
low = float('inf')
for x in nums:
if x % 2 == 1:
x *= 2 # an odd number's largest reachable value
heapq.heappush(heap, -x)
low = min(low, x)
best = float('inf')
while True:
high = -heapq.heappop(heap)
best = min(best, high - low)
if high % 2 == 1: # the max cannot shrink any further
return best
high //= 2
low = min(low, high)
heapq.heappush(heap, -high)
// java
// LC 1675 - Minimize Deviation in Array
// IDEA: normalise odds up (x2); max-heap of current values; halve the max while it is even
// time = O(n log n log M), space = O(n) M = the largest value
public int minimumDeviation(int[] nums) {
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
int min = Integer.MAX_VALUE;
for (int x : nums) {
if (x % 2 == 1) {
x *= 2; // an odd number's largest reachable value
}
maxHeap.offer(x);
min = Math.min(min, x);
}
int deviation = Integer.MAX_VALUE;
while (true) {
int max = maxHeap.poll();
deviation = Math.min(deviation, max - min);
if (max % 2 == 1) {
break; // the max cannot shrink any further
}
max /= 2;
min = Math.min(min, max); // the halved value may be the new minimum
maxHeap.offer(max);
}
return deviation;
}
陷阱
- 把小的值乘上去而不是把最大值減半:正規化之後每一步都是減半,而只有把最大值減半
才能縮小
max − min。 - 最小值不在堆頂 — 必須手動追蹤,每次減半之後都要更新(
[2,10,8]要把 8 → 4 → 2 算進去才得到 3)。 - 要在檢查奇數之前記錄偏差:最後那個奇數最大值仍然給出一個合法答案。
雙堆積中位數範例
| LC | 題目 | 所在 | 說明 |
|---|---|---|---|
| LC 295 | Find Median from Data Stream | heap § 模板 4 | 錨點題 |
| LC 480 | Sliding Window Median | monotonic_queue § 模板 4 | 視窗還會移除值 → 有序多重集合,或雙堆積 + 延遲刪除 |
結束時間堆積範例
| LC | 題目 | 所在 | 說明 |
|---|---|---|---|
| LC 1353 | Maximum Number of Events That Can Be Attended | scanning_line_examples § LC 1353 | 每個事件只花掉其範圍中的一天 → 堆積存截止日,而不是結束時間;heap § 模板 6 說明為什麼 LC 253 的計數會高估 |
| LC 2406 | Divide Intervals Into Minimum Number of Groups | scanning_line_examples § LC 2406 | 閉區間版的 LC 253:只有 end < start 時才沿用一組 |
連續長度上限範例
Reorganize String — LC 767 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
關鍵 — 有解若且唯若最頻繁的字母放得進每隔一格:
max_freq ≤ (n + 1) // 2。 成立之後,每一輪拿最頻繁的兩個字母:它們不同,而且下一輪的第一個字母不可能是 這一輪的第二個,因為第二個的計數永遠不會超過第一個。
歸約到:heap § 模板 7 — 連續長度上限為 1,一次 pop 兩個取代了往回看的檢查。
# python
# LC 767 - Reorganize String
# IDEA: feasible iff max_freq <= (n + 1) // 2; then pop the two most frequent letters each round
# time = O(n log 26) = O(n), space = O(26)
import heapq
from collections import Counter
def reorganizeString(s):
count = Counter(s)
if max(count.values()) > (len(s) + 1) // 2:
return "" # the most frequent letter cannot be spread out
heap = [(-cnt, ch) for ch, cnt in count.items()]
heapq.heapify(heap)
res = []
while len(heap) >= 2:
cnt1, ch1 = heapq.heappop(heap) # the two most frequent letters, different by construction
cnt2, ch2 = heapq.heappop(heap)
res.append(ch1)
res.append(ch2)
if cnt1 + 1 < 0:
heapq.heappush(heap, (cnt1 + 1, ch1))
if cnt2 + 1 < 0:
heapq.heappush(heap, (cnt2 + 1, ch2))
if heap:
res.append(heap[0][1]) # one letter with one copy left
return "".join(res)
// java
// LC 767 - Reorganize String
// IDEA: feasible iff maxFreq <= (n + 1) / 2; then poll the two most frequent letters each round
// time = O(n log 26) = O(n), space = O(26)
public String reorganizeString(String s) {
int[] freq = new int[26];
for (char c : s.toCharArray()) {
freq[c - 'a']++;
}
int n = s.length();
for (int f : freq) {
if (f > (n + 1) / 2) {
return ""; // the most frequent letter cannot be spread out
}
}
PriorityQueue<int[]> maxHeap = new PriorityQueue<>((a, b) -> Integer.compare(b[0], a[0])); // {count, letter}
for (int i = 0; i < 26; i++) {
if (freq[i] > 0) {
maxHeap.offer(new int[]{freq[i], i});
}
}
StringBuilder sb = new StringBuilder();
while (maxHeap.size() >= 2) {
int[] first = maxHeap.poll(); // the two most frequent, different letters
int[] second = maxHeap.poll();
sb.append((char) (first[1] + 'a'));
sb.append((char) (second[1] + 'a'));
if (--first[0] > 0) maxHeap.offer(first);
if (--second[0] > 0) maxHeap.offer(second);
}
if (!maxHeap.isEmpty()) {
sb.append((char) (maxHeap.poll()[1] + 'a')); // one letter with one copy left
}
return sb.toString();
}
第二種解法 — 不用堆積,O(n)。 可行性檢查本身就能建出答案:把字母依頻率由高到低, 先寫進第 0、2、4… 格,再寫進第 1、3、5… 格。同一個字母的兩個複本相隔兩格,而從偶數格 繞到奇數格的地方是安全的,因為檢查保證最頻繁的字母放得進偶數格。
# python
# LC 767 - Reorganize String (even/odd fill)
# IDEA: most frequent letter first into the even slots, then wrap to the odd ones
# time = O(n), space = O(n)
from collections import Counter
def reorganizeString_fill(s):
count = Counter(s)
if max(count.values()) > (len(s) + 1) // 2:
return ""
res = [''] * len(s)
i = 0
for ch, cnt in count.most_common(): # most frequent first: it must fit the even slots
for _ in range(cnt):
if i >= len(s):
i = 1 # even slots used up: continue on the odd ones
res[i] = ch
i += 2
return "".join(res)
陷阱
- 跳過可行性檢查:
"aaab"必須回傳"";一次兩個的迴圈反而會剩下a被連放兩次。 - 偶奇填格只有在最頻繁的字母先放時才成立 —
"abbccc"依字母順序填,會在繞回處讓 兩個c相鄰。 - 堆積最多 26 筆,所以
log 26是常數;說 O(n) 就好。
類似 — LC 1054(同一題換成數字) · LC 358(一般的間隔 k → 模板 8)
冷卻佇列範例
Task Scheduler — LC 621 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現
關鍵 — 只問長度,所以只有最頻繁的任務重要:它逼出
max_freq − 1個寬度為n + 1的框,再加上最後一個框,放所有計數並列最多的任務。如果任務數比這個排法的格子還多, 就永遠不需要閒置,答案是len(tasks)。
歸約到:heap § 模板 8 — 冷卻排程,用封閉公式數出來而不是模擬。
tasks = AAABBB, n = 2 max_freq = 3, tied for it = 2 (A and B)
| A B idle | A B idle | A B
\_ n + 1 _/\_ n + 1 _/\_ 2 _/
slots = (3 - 1) * (2 + 1) + 2 = 8
# python
# LC 621 - Task Scheduler
# IDEA: (max_freq - 1) frames of width n + 1, plus every task tied for max_freq; never below len(tasks)
# time = O(N), space = O(26)
from collections import Counter
def leastInterval(tasks, n):
count = Counter(tasks)
max_freq = max(count.values())
num_max = sum(1 for f in count.values() if f == max_freq) # tasks tied for the max
frames = (max_freq - 1) * (n + 1) + num_max
return max(len(tasks), frames) # enough distinct tasks fill every gap: no idle
// java
// LC 621 - Task Scheduler
// IDEA: (maxFreq - 1) frames of width n + 1, plus every task tied for maxFreq; never below tasks.length
// time = O(N), space = O(26)
public int leastInterval(char[] tasks, int n) {
int[] freq = new int[26];
int maxFreq = 0;
for (char t : tasks) {
freq[t - 'A']++;
maxFreq = Math.max(maxFreq, freq[t - 'A']);
}
int numMax = 0; // tasks tied for the max
for (int f : freq) {
if (f == maxFreq) numMax++;
}
int frames = (maxFreq - 1) * (n + 1) + numMax;
return Math.max(tasks.length, frames); // enough distinct tasks fill every gap: no idle
}
為什麼公式是 V0 — 它是 O(N) 時間、O(1) 空間,是面試官問「回傳最少的時間單位數」時預期的答案, 而且那張框的圖就是一個畫得出來的證明。下面的模擬就是模板 8 本身;當題目要的是任務的順序, 或冷卻規則不一致時,才用它。
第二種解法 — 用堆積加冷卻佇列模擬。 執行過的任務帶著可以再執行的時間在 FIFO 裡等; 到了那個時刻就回到堆積。
# python
# LC 621 - Task Scheduler (simulation)
# IDEA: max-heap of remaining counts; a run task waits in a FIFO until time + n, then returns
# time = O(N * n) in the worst case (idle ticks are simulated), space = O(26)
import heapq
from collections import Counter, deque
def leastInterval_heap(tasks, n):
heap = [-cnt for cnt in Counter(tasks).values()] # most remaining on top
heapq.heapify(heap)
cooldown = deque() # (negated remaining, time it may run again)
time = 0
while heap or cooldown:
time += 1
if heap:
remaining = heapq.heappop(heap) + 1 # run one copy
if remaining < 0:
cooldown.append((remaining, time + n))
if cooldown and cooldown[0][1] == time:
heapq.heappush(heap, cooldown.popleft()[0])
return time
陷阱
- 忘了
max(len(tasks), …):["A","C","A","B","D","B"]、n = 1 的框數是 4, 但六個任務需要六個時間單位。 - 只算一個最多次的任務:
AAABBB、n = 2 是 8 而不是 7 — 最後一個框放的是所有並列max_freq的任務。 - 模擬裡計數取了負號:
remaining < 0表示還有複本;== 0就把這個任務永遠丟掉。
類似 — LC 358(要問順序 → 模板 8) · LC 767(間隔 2,模板 7) · LC 2365(Task Scheduler II:順序固定,用雜湊表記錄下一個允許的日子)