對答案做二分搜尋
範圍 — 針對一個候選答案的範圍,搭配單調的可行性判定式做二分搜尋 —
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(試更大的),並用偏上的 midmid = (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) 內找到那個邊界。
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(最大可行值) |
口訣:
- 最小化:可行時往左走(更小的值)→ 找到最小的可行值
- 最大化:可行時往右走(更大的值)→ 找到最大的可行值
模板上的關鍵差異:
// 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
# 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
// 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。
試跑
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
# 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
// 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)。
試跑
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
# 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
// 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) |
試跑
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
# 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
// 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,從不複製矩陣。
試跑
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 — 必備 — 幾乎每一輪面試都會出現
這是讓二分搜尋能套用到最佳化問題的理論基礎。
單調性(關鍵洞察)
二分搜尋需要單調(已排序)的性質。在「最小化最大值」題目裡,這個性質存在於可行性函式中:
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).
這會形成一條單調的可行性曲線:
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。
證明:
- 答案空間
[left, right]可以切成兩段:[left, answer-1]:全部不可行[answer, right]:全部可行
- 二分搜尋能在 O(log n) 時間內找到這個切分點
- 每一輪都把搜尋空間減半,同時維持不變式
圖解範例 — 可行性曲線
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⁶ 次運算 ✓
對答案做二分搜尋的三項前提
✅ 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
# 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
// 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
# 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
// 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
# 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
// 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
# 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
// 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;
}
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
# 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
// 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
# 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
// 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;
}
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
# 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
// 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
# 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
// 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。
模式選擇策略
如何辨識:
- 題目要求某個「最小/最大」值
- 你可以輕鬆檢查「X 可行嗎?」,卻很難直接算出「最佳的 X 是多少?」
- 答案具有單調性(若 X 可行,X+1 或 X-1 也可行)
常見錯誤:
-
搜尋空間邊界錯誤
- 範圍太窄:會漏掉最佳答案
- 解法:仔細分析下界(例如最大元素)與上界(例如總和)
-
驗證時差一(off-by-one)
java// ❌ WRONG: Using < instead of <= if (currentSum + num < maxSum) {...} // ✅ CORRECT: Must allow equality if (currentSum + num <= maxSum) {...} -
邊界更新寫錯
- 最小化:
right = mid(不是mid - 1) - 最大化:
left = mid,搭配偏上的 mid(l + r + 1) // 2(Java:(int) (((long) l + r + 1) / 2))
- 最小化:
-
驗證函式效率不佳
- 在驗證函式裡加上提早結束
- 用貪婪做法讓驗證維持在 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寫成輔助函式。」