堆積(Heap) — 實戰題解

樹與堆積優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值 更新於 Nov 12, 2026
章節優先度優先度 5/5 — 必備 — 幾乎每一輪面試都會出現必備優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值優先度 3/5 — 值得會 — 多半是必備模式的變形值得會優先度 2/5 — 冷門 — 讀過一次即可,除非目標公司已知會問冷門只標在真正需要的章節上 —— 沒標的是背景/參考資料。

範圍 — 堆積/優先佇列(priority queue)家族的題目集:每題都歸到它所化約成的 heap.md 模板之下,並寫出它額外多出的那一步;模板本身,以及需要進階技巧的題目,放在相鄰的文件裡。 另見 — 主文件:heap.md — 這些範例所實作的標準模板與模式選擇指南。從同一份檔案拆出去的鄰近文件:heap_advanced.md — 延遲刪除(lazy deletion)、反悔貪婪(regret greedy)、掃描線,以及較少見的堆積模板;heap_language_apis.md — 完整的 heapq / PriorityQueue API 參考。

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
# 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
// 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
# 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
// 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
# 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
# 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
# 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
// 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
# 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
// 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
# 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
# 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
// 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
# 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
// 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
# 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
// 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
# 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
// 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
# 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
// 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
# 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 — 冷卻排程,用封閉公式數出來而不是模擬。

text
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
# 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
// 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
# 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:順序固定,用雜湊表記錄下一個允許的日子)