對答案做二分搜尋

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

範圍 — 針對一個候選答案的範圍,搭配單調的可行性判定式做二分搜尋 — canFinish / isValid 的寫法、「最小化最大值 vs 最大化最小值」的取捨、[max(nums), sum(nums)] 邊界配方,以及在值域上做計數的判定式。 另見 — 母文件:binary_search.md — 迴圈不變式、邊界(lower/upper bound)模板、旋轉陣列與二維搜尋;binary_search_examples.md — 索引空間模板的題解存放處。 鄰近文件:greedy.md — 這裡多數判定式都是由貪婪掃描組成的;bfs.md — LC 1631 / LC 778 拿來當判定式的走訪;heap.md — 值域計數之外,找第 k 小元素的另一條路。

LeetCode 題目清單

總覽

關鍵模式 — FAANG 面試中最重要、也最常考的二分搜尋應用之一。

概念

我們不是在陣列裡找某個值,而是對所有可能答案構成的範圍做二分搜尋,再用一個驗證函式檢查該答案是否可行。

什麼時候用:

  • 「找出滿足……的最小/最大值」
  • 「使得……成立的最小/最大 X 是多少?」
  • 「我們能達成 X 嗎?最佳的 X 是多少?」
  • 題目具有單調性:如果 X 可行,那麼 X+1(或 X-1)也一定可行

辨識關鍵字:

  • 「最小化最大值……」
  • 「最大化最小值……」
  • 「找出最小的容量/速度/除數……」
  • 「能不能切分/分配/發放……」

常見題型:

  • LC 410: Split Array Largest Sum
  • LC 1011: Capacity To Ship Packages Within D Days
  • LC 875: Koko Eating Bananas
  • LC 1283: Find the Smallest Divisor
  • LC 1482: Minimum Number of Days to Make m Bouquets
  • LC 2226: Maximum Candies Allocated to K Children

辨識

題目敘述中的訊號 限制條件線索 該用 不適用的情況
「能在……內完成的最小速度/容量/除數/天數」 答案範圍可達 10⁹、n ≤ 10⁵ → O(n log range) 模板 1 最小可行值 答案必須剛好達到某個數量,而不是「至多」→ 判定式不單調
「最大化最小的距離/甜度/糖果數」 同上 模板 2 最大可行值,用偏上的 mid 項目可以任意重排,排序一次再掃一遍就能解 → greedy.md
「把陣列切分/裝運/分配成 k 個連續的組,最小化最大的組和」 n ≤ 1000、值 ≤ 10⁶ 模板 3 [max, sum] 邊界 + 貪婪填裝 分組不必連續(LC 1723)→ 回溯或位元遮罩 DP
排序矩陣、乘法表或數對距離中的「第 k 小」 記憶體要優於 O(n²),所以不能攤平 模板 4 在值域上計數 ≤ mid k 相對於 n 很小 → 用堆積(heap.md)
在排序陣列中「找目標值/第一個 ≥ x 的位置」 不屬於本檔:索引空間的邊界 → binary_search.md

線索欄背後的限制條件量尺在 complexity_cheatsheet.md。


題型分類

模式 1:最小化最大值

  • 目標:找出讓某個最大值 ≤ X 的最小 X
  • 更新:if valid: right = mid(試更小的)
  • 例題:LC 410、1011、1482、2616
  • 關鍵:先排序(如果適用)+ 貪婪驗證
  • 為什麼二分搜尋成立:單調性 — X 越大越容易滿足

模式 2:最大化最小值

  • 目標:找出讓某個最小值 ≥ X 的最大 X
  • 更新:if valid: left = mid(試更大的),並用偏上的 mid mid = (l + r + 1) // 2(Java:(int) (((long) l + r + 1) / 2),加總才不會溢位),迴圈才一定會前進
  • 例題:LC 1231(Divide Chocolate)、LC 1552、LC 2064、LC 2226
  • 關鍵:貪婪驗證 — 我們能不能切出足夠多、每份都 ≥ target 的片段/群組?
  • 為什麼二分搜尋成立:單調性 — X 越小越容易滿足
  • LC 410 的對偶:LC 1231 是「最大化最小甜度」,LC 410 是「最小化最大總和」

模式 3:以計數做驗證

  • 檢查:「我們能在最多 K 組/天/次操作內完成嗎?」
  • 貪婪做法:每一組都盡可能塞滿
  • 例題:LC 410(子陣列)、LC 1011(天數)

模式 4:以總和做驗證

  • 檢查:「總和是否在界限之內?」
  • 累加數值並檢查門檻
  • 例題:LC 1283(除法總和)、LC 875(小時數)

模板與演算法

單調判定式 — 觀念基礎

二分搜尋真正的威力:只要你能定義一個判定式 P(x),使得所有滿足 P 的 x 形成一段連續區間,二分搜尋就能在 O(log n) 內找到那個邊界。

text
P(x) = False, False, ..., False, True, True, ..., True
                                 ^
                          find this boundary

單調判定式的例子:

  • canFinish(speed) — Koko 以 ≥ k 的速度能否在 H 小時內吃完所有香蕉?(LC 875)
  • canShip(capacity) — 用 ≥ c 的容量能否在 D 天內運完所有包裹?(LC 1011)
  • isEnough(mid) — 距離 ≤ mid 的數對是否至少有 k 組?(LC 719)

兩種迴圈。「找出滿足 P 的最小 x」就是 False … True 中的第一個 True: 模板 1,用偏下的 mid。「找出滿足 P 的最大 x」 就是 True … False 中的最後一個 True:模板 2, 用偏上的 mid。下面的決策表把兩者並排比較。

決策表:最小化 vs 最大化 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

目標 例題 可行條件 更新規則 最終答案
最小化最大值 LC 410、1011、2616 若 mid 可行 right = mid(試更小) left(最小可行值)
最大化最小值 LC 1231、1552、2226 若 mid 可行 left = mid(試更大),用偏上的 mid left(最大可行值)

口訣:

  • 最小化:可行時往左走(更小的值)→ 找到最小的可行值
  • 最大化:可行時往右走(更大的值)→ 找到最大的可行值

模板上的關鍵差異:

java
// MINIMIZE pattern (LC 410, 1011)
while (left < right) {
    int mid = left + (right - left) / 2;  // Standard mid
    if (isValid(mid)) right = mid;        // Try smaller
    else left = mid + 1;
}
return left;

// MAXIMIZE pattern (LC 1231, 1552)
while (left < right) {
    int mid = left + (right - left + 1) / 2;  // CRITICAL: +1 to avoid infinite loop!
    if (isValid(mid)) left = mid;             // Try larger
    else right = mid - 1;
}
return left;

為什麼「最大化」模板要 +1? // +1 avoids infinite loop: when left+1==right, (left+right)/2 == left, so left = mid never moves 少了 +1,當 left 與 right 相鄰(left + 1 == right)時,mid 會算成 left。 若 isValid(mid) 為真,我們設 left = mid = left — 沒有前進,無窮迴圈。 加上 +1 讓 mid 往上偏,使 mid == right,保證每一輪區間都會縮小。


模板 1:最小可行值 — LC 875 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

辨識 — 「讓工作仍能完成的最小速度/容量/除數/天數」,而且檢查單一候選很容易、 直接算出最佳值卻不容易。答案範圍可達 10⁹、n ≤ 10⁴–10⁵ → 預期 O(n log range)。

關鍵 — feasible(x) 在 [left, right] 上呈現 False … False True … True,而答案永遠 留在 [left, right] 之內:可行的 mid 保留自己(right = mid),不可行的直接捨棄 (left = mid + 1)。偏下的 mid 讓 right = mid 每次都會縮小範圍,所以迴圈結束時 left == right == 第一個 True。

python
# python
# LC 875 - Koko Eating Bananas
# IDEA: binary search the speed; can_finish(speed) is monotone, so find its first True
# time = O(n log M), space = O(1)   M = max(piles)
def minEatingSpeed(piles, h):
    def can_finish(speed):
        hours = 0
        for pile in piles:
            hours += (pile + speed - 1) // speed   # ceil(pile / speed) without floats
        return hours <= h

    left, right = 1, max(piles)          # speed max(piles) finishes in len(piles) <= h hours
    while left < right:
        mid = left + (right - left) // 2  # lower mid: right = mid always shrinks the range
        if can_finish(mid):
            right = mid                  # mid works -> the answer is mid or smaller
        else:
            left = mid + 1               # mid fails -> the answer is above mid
    return left
java
// java
// LC 875 - Koko Eating Bananas
// IDEA: binary search the speed; canFinish(speed) is monotone, so find its first true
// time = O(n log M), space = O(1)   M = max(piles)
public int minEatingSpeed(int[] piles, int h) {
    int left = 1;
    int right = 0;
    for (int pile : piles) {
        right = Math.max(right, pile);        // speed max(piles) always finishes
    }
    while (left < right) {
        int mid = left + (right - left) / 2;  // lower mid; never overflows
        if (canFinish(piles, h, mid)) {
            right = mid;                      // mid works -> answer is mid or smaller
        } else {
            left = mid + 1;                   // mid fails -> answer is above mid
        }
    }
    return left;
}

private boolean canFinish(int[] piles, int h, int speed) {
    long hours = 0;                           // long: 10^4 piles * 10^9 hours overflows int
    for (int pile : piles) {
        hours += (pile + (long) speed - 1) / speed;   // ceil without floats
    }
    return hours <= h;
}

複雜度

  • 時間 O(n log M):while left < right 迴圈每次把 [1, M] 砍半,共跑 O(log M) 輪, 每輪呼叫一次 can_finish,其中 for pile in piles 迴圈是 O(n)。
  • 空間 O(1):只有 left、right、mid 與累加中的 hours。

試跑

text
piles = [3,6,7,11], h = 8          hours(speed) = sum of ceil(pile / speed)
left=1  right=11  mid=6   hours=1+1+2+2=6  <= 8  -> right=6
left=1  right=6   mid=3   hours=1+2+3+4=10 >  8  -> left=4
left=4  right=6   mid=5   hours=1+2+2+3=8  <= 8  -> right=5
left=4  right=5   mid=4   hours=1+2+2+3=8  <= 8  -> right=4
left=4  right=4   stop -> 4

陷阱

  • 在可行的 mid 上寫 right = mid - 1,可能跨過答案、回傳一個從沒被檢查過的值: piles = [30,11,23,4,20]、h = 6 會回傳 22 而不是 23。要保持 right = mid。
  • left = 0 會在 mid 走到 0 時除以零:piles = [1]、h = 1 會試探速度 0。
  • Java 用 int 累計小時數會溢位:piles = [10^9, 10^9, 10^9]、h = 3 在速度 1 時總共 3·10⁹ 小時, 超過 Integer.MAX_VALUE(約 2.15·10⁹)。 要用 long。
  • 混用兩種寫法:while left <= right 配 right = mid,一旦 left == right 且可行就永遠停不下來。 要嘛 left < right + right = mid,要嘛 left <= right + right = mid - 1 + 一個 ans 變數 — 絕不各取一半。
  • math.ceil(pile / speed) 會經過浮點數;(pile + speed - 1) // speed 才是精確的。

不適用時 — 題目問的是仍然可行的最大值(最大化最小值):True 區塊在左邊, 所以要用 模板 2 與它的偏上 mid。若 x 變大反而 可能讓工作再度失敗(剛好等於某個數量),判定式就不單調,任何二分搜尋都不適用。

練習 — LC 875 ⭐ · LC 1283 (向上取整除法的總和) · LC 1011 (貪婪計算天數;模板 3 的邊界) · LC 1482 (判定式計算長度為 k 的連續段) · LC 2616 (先排序,再貪婪配對)


模板 2:最大可行值 — LC 1552 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

辨識 — 「最大化最小的距離/甜度/每個孩子的糖果數」,或「仍然可行的最大 x」。 這次可行區塊在左邊:能維持 x 的間距,就一定能維持更小的間距。規模和模板 1 相同 → O(n log range)。

關鍵 — feasible(x) 呈現 True … True False … False,而答案留在 [left, right] 之內: 可行的 mid 保留自己(left = mid),不可行的直接捨棄(right = mid - 1)。只有偏上的 mid left + (right - left + 1) // 2,才能在 left + 1 == right 時讓 left = mid 前進。

python
# python
# LC 1552 - Magnetic Force Between Two Balls
# IDEA: binary search the minimum gap; can_place(gap) is monotone (smaller is easier), find its last True
# time = O(n log n + n log R), space = O(n)   R = max(position) - min(position)
def maxDistance(position, m):
    position = sorted(position)          # the greedy placement needs left-to-right order

    def can_place(gap):
        placed = 1                       # the first ball always goes at position[0]
        last = position[0]
        for p in position[1:]:
            if p - last >= gap:          # far enough from the previous ball -> place one
                placed += 1
                last = p
        return placed >= m

    left, right = 1, position[-1] - position[0]   # positions are distinct, so gap 1 always works
    while left < right:
        mid = left + (right - left + 1) // 2      # upper mid: left = mid always shrinks the range
        if can_place(mid):
            left = mid                   # mid works -> the answer is mid or larger
        else:
            right = mid - 1              # mid fails -> the answer is below mid
    return left
java
// java
// LC 1552 - Magnetic Force Between Two Balls
// IDEA: binary search the minimum gap with the upper mid; find the last gap canPlace accepts
// time = O(n log n + n log R), space = O(1) beyond the sort   R = max(position) - min(position)
public int maxDistance(int[] position, int m) {
    Arrays.sort(position);
    int left = 1;
    int right = position[position.length - 1] - position[0];
    while (left < right) {
        int mid = left + (right - left + 1) / 2;   // upper mid: left = mid always moves
        if (canPlace(position, m, mid)) {
            left = mid;                            // mid works -> answer is mid or larger
        } else {
            right = mid - 1;                       // mid fails -> answer is below mid
        }
    }
    return left;
}

private boolean canPlace(int[] position, int m, int gap) {
    int placed = 1;                                // first ball at position[0]
    int last = position[0];
    for (int i = 1; i < position.length; i++) {
        if (position[i] - last >= gap) {           // greedy: place as early as allowed
            placed++;
            last = position[i];
        }
    }
    return placed >= m;
}

複雜度

  • 時間 O(n log n + n log R):排序一次 O(n log n);之後 while left < right 迴圈跑 O(log R) 輪, 每次 can_place 都是一趟 O(n) 掃描。
  • 空間 O(n):Python 要存排序後的副本;Java 原地排序,額外空間 O(1)。

試跑

text
position = [1,2,3,4,7], m = 3
left=1  right=6  mid=4   place 1, 7          -> 2 balls < 3  -> right=3
left=1  right=3  mid=2   place 1, 3, 7       -> 3 balls      -> left=2
left=2  right=3  mid=3   place 1, 4, 7       -> 3 balls      -> left=3
left=3  right=3  stop -> 3

陷阱

  • 偏下的 mid 配 left = mid,範圍剩兩格時就會無窮迴圈:position = [1,2,3]、m = 2 走到 left=1, right=2,mid=1 可行,left 一直停在 1。
  • 忘了排序:position = [5,4,3,2,1,1000000000]、m = 2 會回傳 999999995 而不是 999999999, 因為貪婪把第一顆球放在 5。
  • 貪婪地盡早放球,can_place 才會精確:把球放得比必要的晚,只會壓縮後面剩下的空間。
  • 閉區間寫法(while left <= right,ans = mid; left = mid + 1)也正確,下面的 LC 1231 就是這麼寫。 它不需要偏上的 mid — 但就需要 ans。

不適用時 — 答案是仍然可行的最小值(最小化最大值)→ 模板 1,用偏下的 mid。若最小間距是針對固定的擺放 (沒有選擇球放哪裡的空間),那只是一趟掃描,不是搜尋。

練習 — LC 1552 ⭐ · LC 2226 (糖果:sum(c // x) >= k) · LC 2517 (同一套間距貪婪,用在價格上) · LC 1231 (LC 410 的鏡像:累積到 x 就切) · LC 1802 (可行性是一個等差數列和)


模板 3:切成至多 K 個連續組 — LC 410 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

辨識 — 「把陣列切分/裝運/分配成 k 個連續的子陣列、天數或工人,並最小化最大的組和」。 n ≤ 1000 且值 ≤ 10⁶(LC 410),或 n ≤ 5·10⁴(LC 1011)→ O(n log sum)。

關鍵 — 答案落在 [max(nums), sum(nums)]:低於 max(nums) 時最大的元素放不進任何一組, 而 sum(nums) 就是把所有東西放進同一組。對上限 x,每組一直填到下一個元素會溢出為止, 用到的組數是任何切法中最少的,所以「能放進 ≤ k 組」對 x 是單調的,模板 1 的迴圈就能找到 放得下的最小上限。

python
# python
# LC 410 - Split Array Largest Sum
# IDEA: binary search the cap in [max(nums), sum(nums)]; greedy fill counts the fewest groups for a cap
# time = O(n log S), space = O(1)   S = sum(nums)
def splitArray(nums, k):
    def can_split(cap):
        groups = 1
        current = 0
        for num in nums:
            if current + num <= cap:     # num still fits in the current group
                current += num
            else:                        # close the group, start a new one with num
                groups += 1
                current = num
                if groups > k:           # early exit: already too many groups
                    return False
        return True

    left = max(nums)                     # a group must hold the largest element on its own
    right = sum(nums)                    # one group holding everything always works
    while left < right:
        mid = left + (right - left) // 2
        if can_split(mid):
            right = mid                  # fits in <= k groups -> try a smaller cap
        else:
            left = mid + 1               # needs > k groups -> the cap must grow
    return left
java
// java
// LC 410 - Split Array Largest Sum
// IDEA: binary search the cap in [max(nums), sum(nums)]; greedy fill counts the fewest groups for a cap
// time = O(n log S), space = O(1)   S = sum(nums)
public int splitArray(int[] nums, int k) {
    int left = 0;
    int right = 0;
    for (int num : nums) {
        left = Math.max(left, num);   // lower bound: a group must hold the largest element
        right += num;                 // upper bound: one group holds everything
    }
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (canSplit(nums, k, mid)) {
            right = mid;              // fits -> try a smaller cap
        } else {
            left = mid + 1;           // too many groups -> the cap must grow
        }
    }
    return left;
}

private boolean canSplit(int[] nums, int k, int cap) {
    int groups = 1;
    int current = 0;
    for (int num : nums) {
        if (current + num <= cap) {
            current += num;
        } else {
            groups++;
            current = num;
            if (groups > k) {
                return false;
            }
        }
    }
    return true;
}

複雜度

  • 時間 O(n log S):while left < right 迴圈把 [max, sum] 砍半,共 O(log S) 輪, 每次 can_split 是一趟 O(n) 掃描。
  • 空間 O(1):貪婪只記 groups 與 current。

各題的邊界 — 同一套迴圈,範圍由「一個候選值代表什麼」決定:

LC 題目 最小化的對象 left right
410 Split Array Largest Sum 最大子陣列和 max(nums) sum(nums)
1011 Capacity To Ship Packages 船的容量 max(weights) sum(weights)
875 Koko Eating Bananas 進食速度 1 max(piles)
1283 Find the Smallest Divisor 除數 1 max(nums)
1482 Min Days to Make m Bouquets 天數 min(bloomDay) max(bloomDay)
2064 Minimized Maximum of Products Distributed 每家店的上限 1 max(quantities)

試跑

text
nums = [7,2,5,10,8], k = 2           search [10, 32]
left=10  right=32  mid=21   [7,2,5]=14 [10,8]=18        2 groups -> right=21
left=10  right=21  mid=15   [7,2,5]=14 [10] [8]         3 groups -> left=16
left=16  right=21  mid=18   [7,2,5]=14 [10,8]=18        2 groups -> right=18
left=16  right=18  mid=17   [7,2,5]=14 [10] [8]         3 groups -> left=18
left=18  right=18  stop -> 18

陷阱

  • left = min(nums) 或 1 會讓 can_split 開出一個一開始就超過上限的組,於是少算組數: nums = [1,4,4]、k = 3 會回傳 1 而不是 4。要從 max(nums) 開始。
  • 用 current + num < cap 而不是 <=,會拒絕剛好等於上限的組:[7,2,5,10,8]、 k = 2 會回傳 19 而不是 18。
  • can_split 回答的是「至多 k 組」,不是「剛好 k 組」:只要 k <= n,任何較少組的切法都能 再切細而不提高最大值。
  • 值很大的變體中 right 可能超過 int:10⁵ 個 10⁹ 的總和。要用 long。

不適用時 — 分組不必連續(把工作分給 k 個工人,LC 1723):貪婪填裝不再算出最少組數, 所以要用回溯或位元遮罩 DP。若成本是各組成本的總和而不是最大的那組,就是切分型 DP(dp.md)。

練習 — LC 410 ⭐ · LC 1011 (同一題,換成包裹與天數) · LC 2064 (每家店一組:ceil(q / x)) · LC 1231 (最大化最小值:模板 2)


模板 4:在值域上計數至多 mid — LC 378 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

辨識 — 某個你負擔不起逐一列出的集合中的「第 k 小」:每列每行都排序的 n × n 矩陣、 乘法表、所有數對距離。「記憶體要優於 O(n²)」,或值可達 10⁹、但有便宜的方法數出有多少 ≤ x。

關鍵 — count(x) = 有多少值 ≤ x,隨 x 增加永不遞減,而且只會在真實存在的值上跳升。 所以使 count(x) ≥ k 的最小 x 就是第 k 小,而且一定是真實元素:搜尋值域而非索引, 套用模板 1 的迴圈。

python
# python
# LC 378 - Kth Smallest Element in a Sorted Matrix
# IDEA: binary search the value range; first x with count(<= x) >= k is the answer
# time = O(n log(max - min)), space = O(1)
def kthSmallest(matrix, k):
    n = len(matrix)

    def count_at_most(x):
        # staircase from the bottom-left corner: each step drops a row or a column
        row, col = n - 1, 0
        count = 0
        while row >= 0 and col < n:
            if matrix[row][col] <= x:
                count += row + 1         # this cell and every cell above it are <= x
                col += 1
            else:
                row -= 1
        return count

    left, right = matrix[0][0], matrix[n - 1][n - 1]
    while left < right:
        mid = left + (right - left) // 2
        if count_at_most(mid) >= k:
            right = mid                  # at least k values <= mid -> answer is mid or smaller
        else:
            left = mid + 1               # fewer than k -> answer is above mid
    return left
java
// java
// LC 378 - Kth Smallest Element in a Sorted Matrix
// IDEA: binary search the value range; first x with countAtMost(x) >= k is the answer
// time = O(n log(max - min)), space = O(1)
public int kthSmallest(int[][] matrix, int k) {
    int n = matrix.length;
    int left = matrix[0][0];
    int right = matrix[n - 1][n - 1];
    while (left < right) {
        int mid = left + (right - left) / 2;   // floors even for negatives; (left + right) / 2 does not
        if (countAtMost(matrix, mid) >= k) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }
    return left;
}

private int countAtMost(int[][] matrix, int x) {
    int n = matrix.length;
    int row = n - 1;          // start bottom-left
    int col = 0;
    int count = 0;
    while (row >= 0 && col < n) {
        if (matrix[row][col] <= x) {
            count += row + 1;  // the whole column above is <= x too
            col++;
        } else {
            row--;
        }
    }
    return count;
}

複雜度

  • 時間 O(n log(max − min)):while left < right 迴圈把值域砍半,每次 count_at_most 是一趟階梯走法,最多往上或往右走 2n 步。
  • 空間 O(1):走法只記 row、col 與 count,從不複製矩陣。

試跑

text
matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8
left=1   right=15  mid=8    values <= 8: 1,5                      count=2 < 8  -> left=9
left=9   right=15  mid=12   1,5,9,10,11,12                         count=6 < 8  -> left=13
left=13  right=15  mid=14   1,5,9,10,11,12,13,13                   count=8 >= 8 -> right=14
left=13  right=14  mid=13   same eight                             count=8 >= 8 -> right=13
left=13  right=13  stop -> 13          (mid=14 also had count 8, but 14 is not in the matrix)

陷阱

  • 一看到 count(mid) == k 就回傳 mid:上面的試跑中 mid=14 的計數是 8,但 14 不在矩陣裡。 要繼續收斂到第一個這樣的值。
  • Java 的 (left + right) / 2 對負數會往零截斷:[[-5,-4],[-4,-3]]、k = 2 走到 left=-5, right=-4 時得到 mid=-4,設 right=-4 後就再也不動。要用 left + (right - left) / 2。
  • 從左上角開始走階梯:兩個鄰居都比較大,一步無法決定方向。要從左下角(或右上角)開始。

不適用時 — k 相對於 n² 很小:最小堆積 pop k 次,O(k log n)(heap.md)。 若值沒有排序但有界,計數依然可用,只是檢查方式不同 — 下面的 LC 287 在 [1, n] 上二分, 用鴿籠檢查 count(mid) > mid。

練習 — LC 378 ⭐ · LC 287 (count > mid 代表重複的數 ≤ mid) · LC 668 (count = Σ min(x // i, n)) · LC 719 (排序 + 雙指標數出距離 ≤ x 的數對) · LC 786 (分數:在實數範圍上搜尋)

相似題目

LC 題目 關聯
668 Kth Smallest Number in Multiplication Table count(x) = Σ min(x // i, n)
719 Find K-th Smallest Pair Distance 用雙指標數出距離 ≤ x 的數對
786 K-th Smallest Prime Fraction 對實數值二分 + 計數
373 Find K Pairs with Smallest Sums 同一個第 k 小問題的堆積版本
240 Search a 2D Matrix II 同一套階梯走法,用來搜尋而非計數
4 Median of Two Sorted Arrays 用切分型二分搜尋找第 k 小

為什麼二分搜尋能解「最小化最大值」 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

這是讓二分搜尋能套用到最佳化問題的理論基礎。

單調性(關鍵洞察)

二分搜尋需要單調(已排序)的性質。在「最小化最大值」題目裡,這個性質存在於可行性函式中:

text
If we can achieve the goal with maximum value = X,
then we can ALWAYS achieve it with maximum value = X + 1 (or any larger value).

這會形成一條單調的可行性曲線:

text
Answer Space:  0   1   2   3   4   5   6   7   8   9   ...
               |---|---|---|---|---|---|---|---|---|---|
Feasible?      ✗   ✗   ✗   ✗   ✓   ✓   ✓   ✓   ✓   ✓   ...
                           ↑
                    Decision Boundary (Answer = 4)

The feasibility function is monotonic:
- All values LEFT of boundary: INFEASIBLE (✗)
- All values RIGHT of boundary: FEASIBLE (✓)
- We want to find the LEFTMOST ✓ (minimum feasible value)

為什麼這讓二分搜尋可行

標準二分搜尋在已排序陣列中找目標值。 對答案做二分搜尋則是在已排序的可行性函式中找邊界。

概念 標準二分搜尋 對答案做二分搜尋
搜尋空間 已排序的數值陣列 所有可能答案的範圍
單調性 值是排序好的 可行性是單調的
目標 找到確切的目標值 找到邊界(第一個 ✓)
檢查 nums[mid] == target? isValid(mid)?

數學證明

定理: 若 isValid(x) 具備單調性:

  • isValid(x) = true ⟹ isValid(x + 1) = true

則二分搜尋能正確找出最小的可行 x。

證明:

  1. 答案空間 [left, right] 可以切成兩段:
    • [left, answer-1]:全部不可行
    • [answer, right]:全部可行
  2. 二分搜尋能在 O(log n) 時間內找到這個切分點
  3. 每一輪都把搜尋空間減半,同時維持不變式

圖解範例 — 可行性曲線

text
Problem: Find p=2 pairs with minimum maximum difference
Array after sorting: [1, 1, 2, 3, 7, 10]

Answer space (max diff): 0  1  2  3  4  5  6  7  8  9
Can form 2 pairs?        ✗  ✓  ✓  ✓  ✓  ✓  ✓  ✓  ✓  ✓
                            ↑
                     Answer = 1 (minimum max diff)

Why monotonic?
- If max_diff = 1 works: pairs (1,1)=0, (2,3)=1 → both ≤ 1 ✓
- If max_diff = 2 works: same pairs still work, more options available ✓
- If max_diff = 0 fails: only (1,1)=0 works, can't form 2 pairs ✗

Larger max_diff → More pairs possible → Easier to satisfy constraint

為什麼不直接暴力列舉?(複雜度分析)

做法 時間複雜度 說明
線性搜尋 O(range × n) 檢查每一個可能的答案
二分搜尋 O(log(range) × n) 每次把搜尋空間減半

以 LC 2616 為例:range = 10⁹、n = 10⁵

  • 線性:10⁹ × 10⁵ = 10¹⁴ 次運算 ❌ TLE
  • 二分:log(10⁹) × 10⁵ ≈ 30 × 10⁵ = 3×10⁶ 次運算 ✓

對答案做二分搜尋的三項前提

text
✅ Requirement 1: BOUNDED answer space
   - Must have clear [min, max] range
   - Example: [0, max_element - min_element]

✅ Requirement 2: MONOTONIC feasibility
   - If X works, X+1 must also work (for minimize)
   - If X works, X-1 must also work (for maximize)

✅ Requirement 3: EFFICIENT validation
   - Can check if answer X is valid in O(n) or O(n log n)
   - Usually uses greedy approach

LC 範例

下面每張卡片都歸約到上方四個模板之一,依模板順序分組。

最小可行值範例

Find the Smallest Divisor Given a Threshold — LC 1283

關鍵 — 除數變大,sum(ceil(num / d)) 絕不會變大,所以「總和不超過門檻」在 d 上是 False … True。搜尋 [1, max(nums)]:在 max(nums) 時每一項都是 1,而題目保證 threshold >= len(nums)。

歸約到:模板 1 — 就是 LC 875 的迴圈,把速度換成除數;判定式同樣是上取整除法的總和。

python
# python
# LC 1283 - Find the Smallest Divisor Given a Threshold
# IDEA: first divisor in [1, max(nums)] whose sum of ceil(num / d) is <= threshold
# time = O(n log M), space = O(1)   M = max(nums)
def smallestDivisor(nums, threshold):
    def fits(divisor):
        total = 0
        for num in nums:
            total += (num + divisor - 1) // divisor   # ceil without floats
        return total <= threshold

    left, right = 1, max(nums)
    while left < right:
        mid = left + (right - left) // 2
        if fits(mid):
            right = mid
        else:
            left = mid + 1
    return left
java
// java
// LC 1283 - Find the Smallest Divisor Given a Threshold
// IDEA: first divisor whose sum of ceil(num / d) is <= threshold
// time = O(n log M), space = O(1)   M = max(nums)
public int smallestDivisor(int[] nums, int threshold) {
    int left = 1, right = 0;
    for (int num : nums) right = Math.max(right, num);
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (divisionSum(nums, mid) <= threshold) right = mid;
        else left = mid + 1;
    }
    return left;
}

private long divisionSum(int[] nums, int divisor) {
    long total = 0;
    for (int num : nums) total += (num + divisor - 1) / divisor;
    return total;
}

陷阱

  • left = 0 會在第一次小探測時除以零:從 1 開始。
  • 用 num // divisor(下取整)會少算:nums = [1,2,5,9]、threshold = 6 回傳 3 而不是 5。

Minimum Number of Days to Make m Bouquets — LC 1482

關鍵 — 到第 d 天,所有 bloomDay <= d 的花都開了,天數越多只會開得越多。所以「第 d 天能剪出 m 束、每束 k 朵相鄰的花」是單調的;用一個由左到右的連續計數器數花束。

歸約到:模板 1 — 搜尋天數 [min(bloomDay), max(bloomDay)],並先檢查不可能的情況(m * k > n)。

python
# python
# LC 1482 - Minimum Number of Days to Make m Bouquets
# IDEA: first day d where runs of k open flowers make at least m bouquets
# time = O(n log D), space = O(1)   D = max(bloomDay) - min(bloomDay)
def minDays(bloomDay, m, k):
    if m * k > len(bloomDay):
        return -1                            # not enough flowers on any day

    def can_make(day):
        bouquets = 0
        run = 0                              # adjacent open flowers so far
        for bloom in bloomDay:
            if bloom <= day:
                run += 1
                if run == k:
                    bouquets += 1
                    run = 0                  # these k flowers are used
            else:
                run = 0                      # a closed flower breaks adjacency
        return bouquets >= m

    left, right = min(bloomDay), max(bloomDay)
    while left < right:
        mid = left + (right - left) // 2
        if can_make(mid):
            right = mid
        else:
            left = mid + 1
    return left
java
// java
// LC 1482 - Minimum Number of Days to Make m Bouquets
// IDEA: first day where runs of k open flowers make at least m bouquets
// time = O(n log D), space = O(1)
public int minDays(int[] bloomDay, int m, int k) {
    if ((long) m * k > bloomDay.length) return -1;   // m * k overflows int at 10^6 * 10^6
    int left = Integer.MAX_VALUE, right = 0;
    for (int day : bloomDay) {
        left = Math.min(left, day);
        right = Math.max(right, day);
    }
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (canMake(bloomDay, m, k, mid)) right = mid;
        else left = mid + 1;
    }
    return left;
}

private boolean canMake(int[] bloomDay, int m, int k, int day) {
    int bouquets = 0, run = 0;
    for (int bloom : bloomDay) {
        if (bloom <= day) {
            run++;
            if (run == k) {
                bouquets++;
                run = 0;
            }
        } else {
            run = 0;
        }
    }
    return bouquets >= m;
}

陷阱

  • 遇到未開的花忘了把 run 歸零,會把不相鄰的花算在一起: [7,7,7,7,12,7,7]、m = 2、k = 3 回傳 7 而不是 12。
  • Java 中 m * k 會溢位 int(兩者都可到 10⁶):用 long 比較。

Kth Missing Positive Number — LC 1539

關鍵 — 在嚴格遞增的正整數陣列裡,索引 i 之前缺了 arr[i] − (i + 1) 個數,而這個數量 絕不會變小。第一個讓它達到 k 的索引,答案之前恰好有 left 個陣列中的數,所以答案是 left + k。

歸約到:模板 1 — 找 missing(i) >= k 的第一個 True,搜尋的是索引 [0, n] 而不是值域。

python
# python
# LC 1539 - Kth Missing Positive Number
# IDEA: first index i with arr[i] - (i + 1) >= k; the answer is i + k
# time = O(log n), space = O(1)
def findKthPositive(arr, k):
    left, right = 0, len(arr)                # right = n: the answer can fall past the last element
    while left < right:
        mid = left + (right - left) // 2
        if arr[mid] - (mid + 1) < k:
            left = mid + 1                   # fewer than k missing before mid
        else:
            right = mid
    return left + k                          # left numbers of arr are below the answer
java
// java
// LC 1539 - Kth Missing Positive Number
// IDEA: first index with arr[i] - (i + 1) >= k; the answer is that index + k
// time = O(log n), space = O(1)
public int findKthPositive(int[] arr, int k) {
    int left = 0, right = arr.length;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] - (mid + 1) < k) left = mid + 1;
        else right = mid;
    }
    return left + k;
}

陷阱

  • right = n − 1 會漏掉落在陣列之後的答案:arr = [1,2,3,4]、k = 2 必須回傳 6。

相似題 — LC 1060 (缺數從 nums[0] 起算:binary_search_examples.md)

Minimize the Maximum Difference of Pairs — LC 2616 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

關鍵 — 排序後,最佳的配對組合只會用到相鄰的元素。對上限 x,由左到右的貪婪只要 差值 <= x 就取 (i, i + 1)(然後跳過 i + 1),能配出最多對,所以「在 x 之下配得出 p 對」是單調的。

歸約到:模板 1 — 先排序,搜尋 [0, max − min],判定式是貪婪配對的數量。

python
# python
# LC 2616 - Minimize the Maximum Difference of Pairs
# IDEA: sort; first cap x in [0, max - min] for which greedy adjacent pairing finds p pairs
# time = O(n log n + n log R), space = O(1) besides the sort   R = max - min
def minimizeMax(nums, p):
    nums.sort()

    def enough_pairs(cap):
        count = 0
        i = 0
        while i < len(nums) - 1:
            if nums[i + 1] - nums[i] <= cap:
                count += 1
                i += 2                       # both elements are used
            else:
                i += 1
        return count >= p

    left, right = 0, nums[-1] - nums[0]
    while left < right:
        mid = left + (right - left) // 2
        if enough_pairs(mid):
            right = mid
        else:
            left = mid + 1
    return left
java
// java
// LC 2616 - Minimize the Maximum Difference of Pairs
// IDEA: sort; first cap for which greedy adjacent pairing finds p pairs
// time = O(n log n + n log R), space = O(1) besides the sort
public int minimizeMax(int[] nums, int p) {
    Arrays.sort(nums);
    int left = 0, right = nums[nums.length - 1] - nums[0];
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (enoughPairs(nums, p, mid)) right = mid;
        else left = mid + 1;
    }
    return left;
}

private boolean enoughPairs(int[] nums, int p, int cap) {
    int count = 0;
    int i = 0;
    while (i < nums.length - 1) {
        if (nums[i + 1] - nums[i] <= cap) {
            count++;
            i += 2;                      // both elements are used
        } else {
            i += 1;
        }
    }
    return count >= p;
}
text
nums = [10,1,2,7,1,3], p = 2  -> sorted [1,1,2,3,7,10]
left=0  right=9  mid=4   pairs (1,1) (2,3)        2 >= 2 -> right=4
left=0  right=4  mid=2   pairs (1,1) (2,3)        2 >= 2 -> right=2
left=0  right=2  mid=1   pairs (1,1) (2,3)        2 >= 2 -> right=1
left=0  right=1  mid=0   pairs (1,1)              1 <  2 -> left=1
left=1  right=1  stop -> 1

陷阱

  • 取一對之後沒跳過 i + 1,會重複使用元素:[1,2,3,10]、p = 2 在上限 1 時算出 (1,2) 與 (2,3),回傳 1 而不是 7。
  • 先挑全域最小的差值(用 heap)是一個配對問題,不是貪婪問題;是二分搜尋讓單趟貪婪就足夠。
  • p = 0 不需要特判:right 可能是 0,迴圈根本不會執行。

Path With Minimum Effort — LC 1631 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

關鍵 — can_reach(limit) =「只走高度差 <= limit 的步,能否從左上走到右下」。上限越大 只會解鎖更多邊,所以它是單調的,一次 BFS 就能在 O(m·n) 內回答。

歸約到:模板 1 — 搜尋 [0, 最高 − 最低],判定式是 BFS 可達性測試,而不是 O(n) 的掃描。

python
# python
# LC 1631 - Path With Minimum Effort
# IDEA: binary search the effort limit; feasibility = BFS that only takes steps with |diff| <= limit
# time = O(m * n * log H), space = O(m * n)   H = max height - min height
from collections import deque

def minimumEffortPath(heights):
    rows, cols = len(heights), len(heights[0])

    def can_reach(limit):
        seen = [[False] * cols for _ in range(rows)]
        seen[0][0] = True
        queue = deque([(0, 0)])
        while queue:
            r, c = queue.popleft()
            if r == rows - 1 and c == cols - 1:
                return True
            for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
                if 0 <= nr < rows and 0 <= nc < cols and not seen[nr][nc]:
                    if abs(heights[nr][nc] - heights[r][c]) <= limit:
                        seen[nr][nc] = True
                        queue.append((nr, nc))
        return False

    highest = max(max(row) for row in heights)
    lowest = min(min(row) for row in heights)
    left, right = 0, highest - lowest
    while left < right:
        mid = left + (right - left) // 2
        if can_reach(mid):
            right = mid
        else:
            left = mid + 1
    return left
java
// java
// LC 1631 - Path With Minimum Effort
// IDEA: binary search the effort limit; feasibility = BFS reachability under the limit
// time = O(m * n * log H), space = O(m * n)
public int minimumEffortPath(int[][] heights) {
    int highest = Integer.MIN_VALUE, lowest = Integer.MAX_VALUE;
    for (int[] row : heights) {
        for (int h : row) {
            highest = Math.max(highest, h);
            lowest = Math.min(lowest, h);
        }
    }
    int left = 0, right = highest - lowest;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (canReach(heights, mid)) right = mid;
        else left = mid + 1;
    }
    return left;
}

private boolean canReach(int[][] heights, int limit) {
    int rows = heights.length, cols = heights[0].length;
    boolean[][] seen = new boolean[rows][cols];
    int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    Deque<int[]> queue = new ArrayDeque<>();
    queue.offer(new int[]{0, 0});
    seen[0][0] = true;
    while (!queue.isEmpty()) {
        int[] cur = queue.poll();
        if (cur[0] == rows - 1 && cur[1] == cols - 1) return true;
        for (int[] d : dirs) {
            int nr = cur[0] + d[0], nc = cur[1] + d[1];
            if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && !seen[nr][nc]
                    && Math.abs(heights[nr][nc] - heights[cur[0]][cur[1]]) <= limit) {
                seen[nr][nc] = true;
                queue.offer(new int[]{nr, nc});
            }
        }
    }
    return false;
}

陷阱

  • 1×1 的格子 effort 是 0:right = 0,迴圈不能執行。
  • 在格子出佇列時才標記 seen(而不是入佇列時),同一格會被放進佇列很多次;結果仍正確,但在平坦的 格子上是 O((m·n)²)。

不適用時 — 面試官要更快的界:用瓶頸鬆弛 max(effort, step) 的 Dijkstra 是 O(m·n log(m·n))(Dijkstra.md)。LC 778(Swim in Rising Water)是同一個搜尋, 只是把步的上限換成格子值的上限。

最大可行值範例

Divide Chocolate — LC 1231 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

關鍵 — 你吃的是 k + 1 塊連續小塊中最不甜的那塊,所以要搜尋最大的 x,使巧克力能切成 至少 k + 1 塊、每塊甜度 >= x。一塊一達到 x 就切,能切出最多塊,而 x 越小只會切出更多。

歸約到:模板 2 — 用偏上的 mid 找最大可行值,範圍 [min(sweetness), sum // (k + 1)];是 LC 410 的鏡像。

python
# python
# LC 1231 - Divide Chocolate
# IDEA: last x for which greedy cutting makes >= k + 1 pieces of sweetness >= x; upper mid
# time = O(n log S), space = O(1)   S = sum(sweetness)
def maximizeSweetness(sweetness, k):
    people = k + 1

    def can_cut(target):
        pieces = 0
        current = 0
        for s in sweetness:
            current += s
            if current >= target:            # this piece is sweet enough: cut it here
                pieces += 1
                current = 0
        return pieces >= people              # leftover sweetness joins the last piece

    left, right = min(sweetness), sum(sweetness) // people
    while left < right:
        mid = left + (right - left + 1) // 2  # upper mid: left = mid must move
        if can_cut(mid):
            left = mid
        else:
            right = mid - 1
    return left
java
// java
// LC 1231 - Divide Chocolate
// IDEA: last x for which greedy cutting makes >= k + 1 pieces of sweetness >= x; upper mid
// time = O(n log S), space = O(1)
public int maximizeSweetness(int[] sweetness, int k) {
    int people = k + 1;
    int left = Integer.MAX_VALUE, sum = 0;
    for (int s : sweetness) {
        left = Math.min(left, s);
        sum += s;
    }
    int right = sum / people;
    while (left < right) {
        int mid = left + (right - left + 1) / 2;
        if (canCut(sweetness, people, mid)) left = mid;
        else right = mid - 1;
    }
    return left;
}

private boolean canCut(int[] sweetness, int people, int target) {
    int pieces = 0, current = 0;
    for (int s : sweetness) {
        current += s;
        if (current >= target) {
            pieces++;
            current = 0;
        }
    }
    return pieces >= people;
}
text
sweetness = [1,2,3,4,5,6,7,8,9], k = 5   -> 6 pieces, search [1, 45 // 6 = 7]
left=1  right=7  mid=4   [1,2,3][4][5][6][7][8][9]   7 pieces >= 6 -> left=4
left=4  right=7  mid=6   [1,2,3][4,5][6][7][8][9]    6 pieces >= 6 -> left=6
left=6  right=7  mid=7   [1,2,3,4][5,6][7][8][9]     5 pieces <  6 -> right=6
left=6  right=6  stop -> 6

陷阱

  • 偏下的 mid 搭配 left = mid,一旦 right = left + 1 就不再前進:[1,2]、k = 0 到了 left = 2, right = 3 後一直探測 2,永遠迴圈。
  • 要求剛好 k + 1 塊是錯的:多出來的塊併進最後一塊並不會降低它的甜度,所以判斷用 >=。

相似題 — LC 1552 (錨定題:球之間的間距) · LC 2226 (sum(c // x) >= k) · LC 410 (最小化最大值的鏡像)

切成 K 組範例

Capacity To Ship Packages Within D Days — LC 1011

關鍵 — 就是 LC 410,把數字換成包裹、把組換成天:容量落在 [max(weights), sum(weights)], 而每天一直裝到下一個包裹會超載為止,用的天數是所有裝法中最少的。

歸約到:模板 3 — 完全不變;只有名稱不同。

python
# python
# LC 1011 - Capacity To Ship Packages Within D Days
# IDEA: first capacity in [max(weights), sum(weights)] whose greedy loading needs <= days days
# time = O(n log S), space = O(1)   S = sum(weights)
def shipWithinDays(weights, days):
    def can_ship(capacity):
        needed = 1
        load = 0
        for weight in weights:
            if load + weight <= capacity:
                load += weight
            else:
                needed += 1                  # this package starts a new day
                load = weight
                if needed > days:
                    return False
        return True

    left, right = max(weights), sum(weights)
    while left < right:
        mid = left + (right - left) // 2
        if can_ship(mid):
            right = mid
        else:
            left = mid + 1
    return left
java
// java
// LC 1011 - Capacity To Ship Packages Within D Days
// IDEA: first capacity in [max, sum] whose greedy loading needs <= days days
// time = O(n log S), space = O(1)
public int shipWithinDays(int[] weights, int days) {
    int left = 0, right = 0;
    for (int weight : weights) {
        left = Math.max(left, weight);   // a day must carry the heaviest package
        right += weight;                 // one day carries everything
    }
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (canShip(weights, days, mid)) right = mid;
        else left = mid + 1;
    }
    return left;
}

private boolean canShip(int[] weights, int days, int capacity) {
    int needed = 1, load = 0;
    for (int weight : weights) {
        if (load + weight <= capacity) {
            load += weight;
        } else {
            needed++;
            load = weight;
            if (needed > days) return false;
        }
    }
    return true;
}

陷阱

  • left = 1 會讓比容量還重的包裹單獨成一天,貪婪卻判定可行:weights = [1, 10]、days = 2 回傳 1 而不是 10。從 max(weights) 開始。

至多計數範例

Find the Duplicate Number — LC 287 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

關鍵 — 陣列沒有排序,所以索引沒有用,但值落在 [1, n]。count(v) = 有幾個值 <= v, 它大於 v 恰好是在重複數 <= v 的時候(鴿籠原理),所以重複數就是第一個 count(v) > v 的 v。

歸約到:模板 4 — 在值域 [1, n] 上計數,比較的對象是 v 本身而不是 k。

python
# python
# LC 287 - Find the Duplicate Number
# IDEA: first value v in [1, n] with count(<= v) > v (pigeonhole); the array is never modified
# time = O(n log n), space = O(1)
def findDuplicate(nums):
    left, right = 1, len(nums) - 1          # values are in [1, n], n = len(nums) - 1
    while left < right:
        mid = left + (right - left) // 2
        count = 0
        for x in nums:
            if x <= mid:
                count += 1
        if count > mid:
            right = mid                     # too many values <= mid: the duplicate is <= mid
        else:
            left = mid + 1                  # the duplicate is above mid
    return left
java
// java
// LC 287 - Find the Duplicate Number
// IDEA: first value v with count(<= v) > v (pigeonhole)
// time = O(n log n), space = O(1)
public int findDuplicate(int[] nums) {
    int left = 1, right = nums.length - 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        int count = 0;
        for (int x : nums) if (x <= mid) count++;
        if (count > mid) right = mid;
        else left = mid + 1;
    }
    return left;
}

陷阱

  • count >= mid 差一:[1,3,4,2,2] 的 count(1) = 1,會錯誤地回報 1。
  • 重複數可能出現兩次以上([2,2,2,2,2]);鴿籠判斷依然成立。

第二種做法 — 把 i -> nums[i] 當成鏈結串列做 Floyd 環偵測,是 O(n) 時間、O(1) 空間,寫完這個 之後就該說出這個界(2_pointers_linkedlist.md)。排序或 hash set 都會 違反「不能修改、O(1) 空間」的規定。


依模式分類的題目

本頁解過的每一題都已歸檔在上方它所歸約的模板底下;練習順序見 Drill Path。 LC 69(Sqrt(x))的題解在 math.md, LC 367、LC 441 與 LC 1060 則在 binary_search_examples.md。


模式選擇策略

如何辨識:

  1. 題目要求某個「最小/最大」值
  2. 你可以輕鬆檢查「X 可行嗎?」,卻很難直接算出「最佳的 X 是多少?」
  3. 答案具有單調性(若 X 可行,X+1 或 X-1 也可行)

常見錯誤:

  1. 搜尋空間邊界錯誤

    • 範圍太窄:會漏掉最佳答案
    • 解法:仔細分析下界(例如最大元素)與上界(例如總和)
  2. 驗證時差一(off-by-one)

    java
    // ❌ WRONG: Using < instead of <=
    if (currentSum + num < maxSum) {...}
    
    // ✅ CORRECT: Must allow equality
    if (currentSum + num <= maxSum) {...}
    
  3. 邊界更新寫錯

    • 最小化:right = mid(不是 mid - 1)
    • 最大化:left = mid,搭配偏上的 mid (l + r + 1) // 2(Java:(int) (((long) l + r + 1) / 2))
  4. 驗證函式效率不佳

    • 在驗證函式裡加上提早結束
    • 用貪婪做法讓驗證維持在 O(n)

面試時可以說的話:

  • 「這是一題對答案空間做二分搜尋的問題」
  • 「我會在 [min, max] 上做二分搜尋,並寫一個輔助函式來驗證」
  • 「答案具有單調性:若 X 可行,更大的 X 也一定可行」
  • 「時間複雜度:O(n × log(range)),其中 n 是驗證的成本」

練習路徑

階段 題目(易 → 難) 新增的內容
核心 ⭐ LC 875 ok ⭐ · LC 1552 again · LC 410 未評定 最小可行值、最大可行值(偏上的 mid),以及 [max, sum] 切分
練習 LC 1283 未評定 · LC 1011 ok · LC 1482 未評定 · LC 2226 未評定 · LC 287 ok · LC 378 again 其他貪婪判定式;接著是在值域上計數
延伸 LC 2616 again · LC 1631 again · LC 1231 again · LC 668 未評定 · LC 719 未評定 先排序再配對、BFS 判定式、用切割來最大化最小值、不列舉也能計數

總結

  • 何時用、怎麼寫 — 前提見 三項要求,兩種迴圈形狀見 決策矩陣;題目該用哪個模板,由 辨識 表決定。
  • 邊界配方 — 「把陣列元素切分/裝運/分配成 K 組並最小化最大值」⇒ left = max(nums)、right = sum(nums)。「速率/除數/天數」⇒ left = 1、right = max(nums)。
  • 同一套骨架,不同的判定式 — 貪婪掃描(LC 410、1011、875、1482)、在值域上計數(LC 378、287、1539),或走一趟圖(LC 1631、LC 778)。
  • 面試時把它講出來:「答案在可行性上是單調的,所以我會對答案空間做二分搜尋,並把 isValid 寫成輔助函式。」