“Binary Search on the Answer” = thay vì tìm kiếm một VALUE trong sorted array, bạn tìm kiếm chính CÂU TRẢ LỜI trong không gian giá trị của nó.
Tưởng tượng bạn cần tìm nhiệt độ tối ưu để nướng bánh mì:
Bạn biết nhiệt độ nằm trong khoảng 100°C – 300°C
Với mỗi nhiệt độ bạn chọn, bạn có thể kiểm tra “có đủ tốt không?” trong O(1)
Không cần thử hết 200 nhiệt độ → binary search nhiệt độ!
Key insight: Không gian câu trả lời có tính monotonic — nếu nhiệt độ X “đủ tốt”, thì X+1 cũng “đủ tốt” (hoặc ngược lại). Tính chất này cho phép binary search.
Classic Binary Search vs Binary Search on Answer:
Classic
On Answer
Tìm VALUE trong sorted array
Tìm ANSWER trong range [lo, hi]
Array đã sắp xếp sẵn
Không gian answer có tính monotonic
O(log N)
O(N log(range)) — N cho check, log cho search
”Is target at index mid?"
"Is mid a feasible answer?”
L5 → L6 Differentiator
Đây là kỹ thuật phân biệt Senior Engineer với Mid-level. Bài toán trông có vẻ cần O(N²) brute force, nhưng khi nhận ra “answer space is monotonic” → O(N log N) solution hiện ra.
2. The FAANG Standard
Framework tổng quát:
1. Xác định không gian answer: [lo, hi]
- lo = giá trị nhỏ nhất có thể (often 1 or min(array))
- hi = giá trị lớn nhất có thể (often max(array) or sum(array))
2. Viết hàm check(mid): có thể đạt được với answer = mid không?
- Hàm này phải chạy trong O(N) hoặc O(N log N)
- Hàm PHẢI monotonic: false...false...true...true (hoặc ngược lại)
3. Binary search trên [lo, hi]:
- Find first true (minimize): lo=mid+1 khi check fails
- Find last true (maximize): hi=mid-1 khi check fails
Các dạng bài phổ biến:
Problem Type
lo
hi
check(mid)
Koko Bananas
1
max(piles)
Ăn hết trong H giờ không?
Min Days Bouquets
1
max_days
Đủ bouquets sau mid ngày không?
Capacity to Ship
max(weights)
sum(weights)
Ship hết trong D ngày không?
Split Array Largest Sum
max(array)
sum(array)
Chia thành m phần, max ≤ mid không?
Kth Smallest Matrix
matrix[0][0]
matrix[n-1][n-1]
Có ≥ k elements ≤ mid không?
3. Visual Thinking (Mermaid)
3.1 Monotonic Predicate Concept
graph LR
subgraph "Answer Space [lo ... hi]"
direction LR
F1["lo: ❌"] --> F2["❌"] --> F3["❌"] --> T1["✅ ← first true"] --> T2["✅"] --> T3["hi: ✅"]
end
style T1 fill:#22c55e,color:#fff
style F1 fill:#ef4444,color:#fff
style F2 fill:#ef4444,color:#fff
style F3 fill:#ef4444,color:#fff
style T2 fill:#22c55e,color:#fff
style T3 fill:#22c55e,color:#fff
flowchart TD
A["Binary Search on [lo=1, hi=max]"] --> B{"check(mid)?"}
B -->|"false — mid too small"| C["lo = mid + 1"]
B -->|"true — mid feasible"| D["hi = mid - 1<br/>(try smaller — minimize)"]
C --> E["Converge to first TRUE"]
D --> E
E --> F["Answer = lo"]
style A fill:#4a9eff,color:#fff
style F fill:#22c55e,color:#fff
3.2 Binary Search on Answer for Min Days to Make Bouquets
sequenceDiagram
participant BS as Binary Search
participant CF as check(mid=days)
Note over BS: bloomDay=[1,10,3,10,2], m=3 bouquets, k=1 flower each
Note over BS: lo=1, hi=10
BS->>CF: check(mid=5)
Note over CF: After 5 days: [1,_,3,_,2] → 3 consecutive? No, only [1],[3],[2] non-consecutive
CF-->>BS: false → lo=6
BS->>CF: check(mid=8)
Note over CF: After 8 days: [1,_,3,_,2] same... wait, only days≤8 bloom
Note over CF: bloomed at days[0]=1,days[2]=3,days[4]=2 → 3 flowers → 3 bouquets ✓
CF-->>BS: true → hi=7
BS->>CF: check(mid=6)
Note over CF: Same — 3 flowers bloom by day 6 ✓
CF-->>BS: true → hi=5
Note over BS: lo=6 > hi=5 → STOP, answer = lo = 3
3.3 Rotated Array — Which Half is Sorted?
graph TD
A["nums=[4,5,6,7,0,1,2], target=0"] --> B{"nums[mid] vs nums[lo]"}
B -->|"nums[mid]=7 >= nums[lo]=4<br/>Left half [4..7] is SORTED"| C{"target in left?"}
C -->|"0 NOT in [4,7]"| D["Go RIGHT: lo=mid+1"]
D --> E["nums=[0,1,2], mid=1"] --> F{"nums[mid]=1 >= nums[lo]=0<br/>Left sorted"}
F -->|"target=0 in [0,1]"| G["Go LEFT: hi=mid-1"]
G --> H["nums=[0], found at idx=4"]
style H fill:#22c55e,color:#fff
4. TypeScript Template
4.1 Universal Binary Search Template
// ============================================================// UNIVERSAL BINARY SEARCH TEMPLATE// Find: smallest mid where check(mid) = true (minimize the answer)// ============================================================function binarySearchTemplate( lo: number, hi: number, check: (mid: number) => boolean): number { while (lo < hi) { const mid = lo + Math.floor((hi - lo) / 2); // avoid overflow if (check(mid)) { hi = mid; // mid is feasible, try smaller → hi = mid } else { lo = mid + 1; // mid not feasible, need larger → lo = mid+1 } } return lo; // first value where check(lo) = true}// For MAXIMIZE (last true): swap the hi/lo updatesfunction binarySearchMaximize( lo: number, hi: number, check: (mid: number) => boolean): number { while (lo < hi) { const mid = lo + Math.ceil((hi - lo) / 2); // NOTE: ceiling to avoid infinite loop if (check(mid)) { lo = mid; // mid is feasible, try larger } else { hi = mid - 1; // mid not feasible, must be smaller } } return lo;}
4.2 Koko Eating Bananas — O(N log M)
// LeetCode #875// Binary search on eating speed k (1 to max(piles))// check(k): can Koko eat all piles within h hours at speed k?function minEatingSpeed(piles: number[], h: number): number { const lo = 1; const hi = Math.max(...piles); // check: with speed=k, total hours = sum of ceil(pile/k) const canFinish = (k: number): boolean => { let hours = 0; for (const pile of piles) { hours += Math.ceil(pile / k); } return hours <= h; }; return binarySearchTemplate(lo, hi, canFinish);}// Example: piles = [3,6,7,11], h = 8// lo=1, hi=11// check(6): ceil(3/6)+ceil(6/6)+ceil(7/6)+ceil(11/6) = 1+1+2+2 = 6 <= 8 ✓// check(3): ceil(3/3)+ceil(6/3)+ceil(7/3)+ceil(11/3) = 1+2+3+4 = 10 > 8 ✗// Answer: 4
4.3 Minimum Days to Make M Bouquets — O(N log D)
// LeetCode #1482function minDays(bloomDay: number[], m: number, k: number): number { const n = bloomDay.length; // Edge case: not enough flowers total if (m * k > n) return -1; const lo = 1; const hi = Math.max(...bloomDay); // check(day): can we make m bouquets after 'day' days? // Each bouquet needs k ADJACENT flowers that have bloomed by 'day' const canMake = (day: number): boolean => { let bouquets = 0; let consecutive = 0; for (let i = 0; i < n; i++) { if (bloomDay[i] <= day) { consecutive++; if (consecutive === k) { bouquets++; consecutive = 0; // reset for next bouquet } } else { consecutive = 0; // streak broken } } return bouquets >= m; }; return binarySearchTemplate(lo, hi, canMake);}
4.4 Split Array Largest Sum — O(N log S)
// LeetCode #410 — See Section 8 for complete solution
4.5 Kth Smallest in Sorted Matrix — O(N log(max-min))
// LeetCode #378// Key: binary search on VALUE space, not index space!// check(mid): how many elements in matrix are <= mid?function kthSmallest(matrix: number[][], k: number): number { const n = matrix.length; const lo = matrix[0][0]; const hi = matrix[n - 1][n - 1]; // Count elements <= mid using sorted property (O(N) with two-pointer on rows) const countLE = (mid: number): number => { let count = 0; let col = n - 1; // start from top-right corner for (let row = 0; row < n; row++) { while (col >= 0 && matrix[row][col] > mid) col--; count += col + 1; // all elements in this row up to col are <= mid } return count; }; // Find smallest value v such that count(v) >= k return binarySearchTemplate(lo, hi, (mid) => countLE(mid) >= k);}// Note: The answer IS in the matrix (not necessarily mid itself).// binarySearchTemplate finds the exact value because when lo=hi,// it must be an actual element (proven by invariant).
4.6 Search in Rotated Sorted Array — O(log N)
// LeetCode #33// Key insight: one half is ALWAYS sorted. Use that to determine which side target is in.function searchRotated(nums: number[], target: number): number { let lo = 0, hi = nums.length - 1; while (lo <= hi) { const mid = lo + Math.floor((hi - lo) / 2); if (nums[mid] === target) return mid; // Left half [lo..mid] is sorted if (nums[lo] <= nums[mid]) { if (nums[lo] <= target && target < nums[mid]) { hi = mid - 1; // target in left sorted half } else { lo = mid + 1; // target in right half (possibly rotated) } } // Right half [mid..hi] is sorted else { if (nums[mid] < target && target <= nums[hi]) { lo = mid + 1; // target in right sorted half } else { hi = mid - 1; // target in left half (possibly rotated) } } } return -1;}
5. Complexity Deep-dive
Key Complexity Insight
log(109)≈30
Nếu check function chạy O(N), thì binary search on answer chạy O(N × 30) = O(N log(max_val)).
Với N = 10^5 và max_val = 10^9: chỉ cần khoảng 3 × 10^6 operations — cực kỳ nhanh!
Problem
Time
Space
Bottleneck
Koko Eating Bananas
O(NlogM)
O(1)
M = max(piles)
Min Days Bouquets
O(NlogD)
O(1)
D = max(bloomDay)
Capacity to Ship
O(NlogS)
O(1)
S = sum(weights)
Split Array Largest Sum
O(NlogS)
O(1)
S = sum(array)
Kth Smallest Matrix
O(Nlog(hi−lo))
O(1)
Matrix range
Search Rotated Array
O(logN)
O(1)
Classic binary search
So sánh với Brute Force:
Split Array Largest Sum: DP solution O(N²·M) vs Binary Search O(N log S)
Với N=1000, M=50: DP = 50,000,000 vs Binary Search ≈ 30,000 operations
6. Edge Cases & Pitfalls
Predicate Must Be Monotonic
Binary search on answer chỉ đúng khi predicate có tính monotonic:
false, false, ..., false, true, true, ..., true
Nếu pattern là false, true, false, true → không thể dùng binary search!
Luôn verify tính monotonic bằng cách chứng minh: “nếu check(x) = true thì check(x+1) = true”.
Getting the Boundary Right
Hai variants boundary quan trọng:
// MINIMIZE (find first true):if (check(mid)) hi = mid; // mid might be answer, don't exclude itelse lo = mid + 1; // mid definitely not answer// Final answer: lo// MAXIMIZE (find last true):if (check(mid)) lo = mid; // mid might be answerelse hi = mid - 1; // mid definitely not answer// Use: mid = lo + Math.ceil((hi-lo)/2) ← ceiling avoids infinite loop!// Final answer: lo (= hi)
Integer vs Float Binary Search
Float binary search cần epsilon thay vì lo < hi:
// Float binary search:let lo = 0.0, hi = 1e9;const eps = 1e-7;while (hi - lo > eps) { // NOT hi > lo (infinite loop!) const mid = (lo + hi) / 2; if (check(mid)) hi = mid; else lo = mid;}return lo;
Rotated Array — Tricky Equal Case
Khi nums[lo] === nums[mid] (với duplicates, e.g., #81):
Không thể determine which half is sorted → must lo++ và try again.
Overflow in Mid Calculation
// WRONG (overflow khi lo + hi > 2^31 - 1):const mid = (lo + hi) / 2;// CORRECT:const mid = lo + Math.floor((hi - lo) / 2);
JavaScript numbers are 64-bit float (safe up to 2^53), nhưng good habit anyway.
7. Pattern Recognition
Khi Nào Dùng Binary Search on Answer
“Minimize the MAXIMUM” → Binary search on max value
→ “Minimize largest sum”, “minimize maximum distance”
→ check(mid): can we achieve max ≤ mid?
“Maximize the MINIMUM” → Binary search on min value
→ “Maximize minimum distance”, “maximize minimum pages”
→ check(mid): can we achieve min ≥ mid?
“K-th smallest in sorted structure” → Binary search on value
→ check(mid): are there ≥ k elements ≤ mid?
“Feasibility with monotonic threshold” → Binary search on threshold
→ “At least X bananas/hour”, “ship within D days”
→ Any problem where check function has monotonic property
“Search in modified sorted array” → Modified binary search
→ Rotated sorted array, bitonic array, search in 2D matrix
// LeetCode #410 — Split Array Largest Sum// Problem: Split array into m non-empty subarrays to MINIMIZE the largest subarray sum.// Input: nums = [7,2,5,10,8], m = 2// Output: 18// (Split into [7,2,5] and [10,8] → max sum = 18; optimal)function splitArray(nums: number[], m: number): number { // ── Define the search space ──────────────────────────────────────────────── // lo: if we could split into nums.length parts, each part is one element // → minimum possible max = max(nums) (can't make any part smaller than its element) // hi: if we use 1 part (no split), max = sum(nums) const lo = Math.max(...nums); const hi = nums.reduce((a, b) => a + b, 0); // ── check(maxSum): Can we split into AT MOST m parts with each sum ≤ maxSum? ── // Greedy: greedily extend current subarray until adding next would exceed maxSum. // Then start a new part. Count total parts needed. const canSplit = (maxSum: number): boolean => { let parts = 1; // always at least 1 part let currentSum = 0; for (const num of nums) { if (currentSum + num > maxSum) { // Can't add num to current part → start a new part parts++; currentSum = num; if (parts > m) return false; // too many parts needed } else { currentSum += num; } } return true; // can split into ≤ m parts with max sum ≤ maxSum }; // ── Binary search: find smallest maxSum where canSplit is true ──────────── return binarySearchTemplate(lo, hi, canSplit);}/*TRACE for nums = [7,2,5,10,8], m = 2:lo = max(7,2,5,10,8) = 10hi = 7+2+5+10+8 = 32Iteration 1: mid = 10 + floor((32-10)/2) = 21 canSplit(21): num=7: currentSum=7 num=2: currentSum=9 num=5: currentSum=14 num=10: currentSum=24 > 21 → parts=2, currentSum=10 num=8: currentSum=18 ≤ 21 ✓ parts=2 ≤ m=2 → true → hi = 21Iteration 2: mid = 10 + floor((21-10)/2) = 15 canSplit(15): num=7: currentSum=7 num=2: currentSum=9 num=5: currentSum=14 num=10: 14+10=24 > 15 → parts=2, currentSum=10 num=8: 10+8=18 > 15 → parts=3 > m=2 → false → lo = 16Iteration 3: mid = 16 + floor((21-16)/2) = 18 canSplit(18): num=7: currentSum=7 num=2: currentSum=9 num=5: currentSum=14 num=10: 14+10=24 > 18 → parts=2, currentSum=10 num=8: 10+8=18 ≤ 18 ✓ parts=2 ≤ m=2 → true → hi = 18Iteration 4: mid = 16 + floor((18-16)/2) = 17 canSplit(17): num=7: currentSum=7 num=2: currentSum=9 num=5: currentSum=14 num=10: 14+10=24 > 17 → parts=2, currentSum=10 num=8: 10+8=18 > 17 → parts=3 > m=2 → false → lo = 18lo=18, hi=18 → STOP, return 18 ✓WHY GREEDY WORKS for canSplit:- We want minimum number of parts for a given maxSum.- Greedy (extend as far as possible) is optimal because: Making a part shorter never helps — it only creates more parts elsewhere. Proof: if any split uses ≤ m parts, greedy also uses ≤ m parts.Time: O(N log S) — S = sum(nums), log S binary search iterations, each O(N) checkSpace: O(1) — only constant extra space*/
9. Self-Assessment Checklist
Checklist sau khi học Advanced Binary Search
Giải thích được “binary search on the answer” concept với ví dụ cụ thể
Implement universal template với lo/hi/check function
Biết khi nào dùng hi = mid vs hi = mid - 1 (minimize vs maximize)
Biết tại sao maximize variant cần Math.ceil cho mid
Giải #875 Koko Eating Bananas không nhìn note
Giải #410 Split Array Largest Sum và giải thích greedy check
Search in Rotated Sorted Array — xác định đúng half nào sorted
Nhận ra “binary search on answer” pattern khi đọc đề bài mới
Chứng minh được tại sao predicate trong một bài cụ thể là monotonic
Luyện tập tiếp theo
#1231 Divide Chocolate (maximize minimum)
#774 Minimize Max Distance to Gas Station
#1552 Magnetic Force Between Two Balls
#2064 Minimized Maximum of Products Distributed to Any Store