Binary Search & Search on Answer โ Ultimate Reference Guide
A single source of truth for binary search โ from the first "guess the number" intuition to allocating resources under constraints, and where the technique quietly powers modern DS, AI/ML, and LLM systems.
Every code block in this guide has been executed and verified against known expected outputs.
Table of Contentsโ
- Intuition & Analogy
- Classical Binary Search
- Search on Answer
- 3.1 Paradigm Explanation
- 3.2 Framework & Template
- 3.3 Worked Problems
- Complexity Analysis
- Applications in DS / AI / ML / LLMs
- Expert Takeaways & Mental Models
- Quick Reference Cheat Sheet
1. Intuition & Analogyโ
The one-sentence ideaโ
Binary search is the art of throwing away half of the remaining possibilities with every single question, because you can tell which half the answer is not in.
Real-world analogiesโ
The dictionary flip. You look up "monotonic." You don't start at page 1. You flip to the middle, land on "M-ish" words, realize you overshot slightly, flip halfway back, and converge in a handful of jumps. A 2,000-page dictionary is conquered in ~11 flips. That halving-per-flip is logโ(2000) โ 11.
The number-guessing game. "I'm thinking of a number between 1 and 100." You guess 50. "Higher." Now it's 51โ100. You guess 75. "Lower." Now 51โ74. Each answer โ higher or lower โ is a monotonic signal that eliminates half the range. You never need more than 7 guesses (logโ(100) โ 6.6).
Why halving beats scanning. Linear scan asks "is it this one?" n times. Binary search asks "is it in the left or right half?" log n times. For a billion items, that's ~30 questions instead of a billion.
The core prerequisite: MONOTONICITYโ
This is the single most important idea in the entire guide, and the one beginners underestimate.
Binary search does not actually require a sorted array. It requires a monotonic predicate โ a yes/no question whose answer, as you move left to right across the search space, flips at most once and never flips back:
Search space: [ F F F F T T T T T ]
โ
the boundary we hunt for
- A sorted array is just the special case where the predicate is
arr[i] >= target. - If your predicate looks like
F F T F T T(flips more than once), binary search is invalid โ it may return a wrong answer silently.
Beginner misunderstanding: "Binary search = searching a sorted array." Expert reframe: "Binary search = locating the boundary of a monotonic true/false region." Sorting is one way to create that monotonicity; it is not the definition.
Complexity at a glanceโ
| Metric | Iterative | Recursive |
|---|---|---|
| Time | O(log n) | O(log n) |
| Space | O(1) | O(log n) (call stack) |
Why O(log n) time? The candidate range starts at size n and halves each step: n โ n/2 โ n/4 โ โฆ โ 1. The number of halvings to reach 1 is logโ n. Formally, the recurrence T(n) = T(n/2) + O(1) solves to T(n) = O(log n) by the Master Theorem.
Why O(1) space (iterative)? You only track lo, hi, mid โ three integers, regardless of input size. The recursive version costs O(log n) stack frames because each call waits on the next.
2. Classical Binary Searchโ
2.1 Algorithm Walkthroughโ
Plain English. Keep a window [lo, hi] that is guaranteed to contain the answer if it exists. Look at the middle element:
- If it is the target โ done.
- If it's too small โ the answer must be to the right โ move
lopastmid. - If it's too big โ the answer must be to the left โ move
hibeforemid. - Repeat until the window is empty (
lo > hi) โ target absent.
The invariant that makes it correct: at all times, everything outside [lo, hi] has already been proven incapable of being the answer. We never re-examine discarded regions, and we never discard the answer.
Worked trace โ find 7 in [1, 3, 5, 7, 9, 11] (verified output):
| Iteration | lo | hi | mid | arr[mid] | Decision |
|---|---|---|---|---|---|
| 1 | 0 | 5 | 2 | 5 | 5 < 7 โ search right, lo = 3 |
| 2 | 3 | 5 | 4 | 9 | 9 > 7 โ search left, hi = 3 |
| 3 | 3 | 3 | 3 | 7 | 7 == 7 โ return index 3 โ
|
Three comparisons for six elements โ and it would still be ~30 for a billion.
2.2 Code Templatesโ
def binary_search(arr, target):
"""Classic exact-match binary search (iterative).
Returns the index of target, or -1 if absent.
Uses the inclusive [lo, hi] convention.
"""
lo, hi = 0, len(arr) - 1
while lo <= hi: # inclusive: lo == hi still has 1 candidate
mid = lo + (hi - lo) // 2 # overflow-safe midpoint
if arr[mid] == target:
return mid
elif arr[mid] < target:
lo = mid + 1 # discard mid and everything left
else:
hi = mid - 1 # discard mid and everything right
return -1 # window empty โ not found
def binary_search_rec(arr, target, lo=0, hi=None):
"""Recursive exact-match binary search. O(log n) stack space."""
if hi is None:
hi = len(arr) - 1
if lo > hi:
return -1
mid = lo + (hi - lo) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
return binary_search_rec(arr, target, mid + 1, hi)
else:
return binary_search_rec(arr, target, lo, mid - 1)
Verified: binary_search([1,3,5,7,9,11], 7) == 3, binary_search([1,3,5,7,9,11], 4) == -1.
2.3 Variants โ Exact / Lower / Upper Boundโ
With duplicates, "find the target" is ambiguous. The professional tools are lower bound and upper bound. Both use the half-open [lo, hi) convention with while lo < hi, which is cleaner and less bug-prone for boundary hunting.
def lower_bound(arr, target):
"""First index i where arr[i] >= target.
Equivalent to Python's bisect.bisect_left.
Returns len(arr) if all elements are < target.
"""
lo, hi = 0, len(arr) # note: hi = len(arr), NOT len-1
while lo < hi:
mid = lo + (hi - lo) // 2
if arr[mid] < target:
lo = mid + 1 # mid too small โ answer strictly right
else:
hi = mid # mid is a candidate โ keep it in [lo, hi)
return lo
def upper_bound(arr, target):
"""First index i where arr[i] > target.
Equivalent to Python's bisect.bisect_right.
"""
lo, hi = 0, len(arr)
while lo < hi:
mid = lo + (hi - lo) // 2
if arr[mid] <= target:
lo = mid + 1
else:
hi = mid
return lo
How they combine (verified on [1, 2, 2, 2, 3, 4, 4, 5]):
| Query | lower_bound | upper_bound | Interpretation |
|---|---|---|---|
| target = 2 | 1 | 4 | value 2 occupies indices [1, 4) โ count = 3 |
| target = 4 | 5 | 7 | value 4 occupies indices [5, 7) โ count = 2 |
- Count of a value =
upper_bound(x) โ lower_bound(x). - Exists? =
lower_bound(x) < len(arr) and arr[lower_bound(x)] == x. - First occurrence =
lower_bound(x); last occurrence =upper_bound(x) โ 1. - Insertion point to keep sorted =
lower_bound(x).
Python ships these as bisect.bisect_left / bisect.bisect_right โ use them in production; the templates above are what they do internally and what you write in interviews or non-Python stacks.
2.4 Common Bugs & Fixesโ
| Bug | Symptom | Fix |
|---|---|---|
| Wrong loop condition | Misses the last element, or off-by-one result | Match condition to convention: inclusive [lo, hi] โ while lo <= hi; half-open [lo, hi) โ while lo < hi. Never mix. |
| Integer overflow | mid = (lo + hi) // 2 overflows in C/C++/Java when lo + hi > INT_MAX (the famous JDK/JGuru bug, 2006) | Always write mid = lo + (hi - lo) // 2. Harmless in Python (bignums), but a career-defining habit everywhere else. |
| Infinite loop | Hangs forever | Ensure the range strictly shrinks every iteration. With hi = mid (not mid - 1), you must also advance lo = mid + 1 on the other branch โ otherwise lo == mid when hi = lo + 1 and it never moves. |
| Updating the wrong pointer | Converges to the wrong side | The pointer you move to mid (vs mid ยฑ 1) must correspond to the branch that keeps mid as a candidate. |
Returning lo vs hi | Off-by-one on absence/insertion | After a [lo, hi) search, lo == hi is the boundary. Return lo. Don't guess. |
| Unsorted / non-monotonic input | Silently wrong answer, no error | Verify the monotonicity precondition. Binary search cannot detect that its precondition is violated. |
The infinite-loop trap in detail. When you use hi = mid (needed for boundary searches), the midpoint mid = lo + (hi - lo)//2 rounds down. If you ever write lo = mid (instead of mid + 1) in the other branch, then when hi = lo + 1, mid == lo, you set lo = mid == lo, and nothing changes โ forever. Rule of thumb: the branch that sets hi = mid pairs with a branch that sets lo = mid + 1. The +1 guarantees progress.
3. Search on Answerโ
3.1 Paradigm Explanationโ
The paradigm shift. In classical binary search you search over indices of an existing array. In search on answer, there is often no array to search โ instead you binary-search over the space of possible answer values, using a feasibility test to decide which half to keep.
Beginner misunderstanding: "Binary search needs an array." Expert reframe: "If I can (a) guess an answer and (b) cheaply check whether that guess works, and (c) the checking result is monotonic in the guess, then I can binary-search the answer itself โ no array required."
The tell-tale signature of a search-on-answer problem:
- The question asks for a minimum or maximum value ("minimum capacity", "maximum speed", "smallest largest sum", "minimum time").
- Directly computing that optimum is hard, but verifying a specific candidate is easy.
- Feasibility is monotonic: if capacity
Xworks, every capacity> Xalso works (or vice versa).
That monotonicity turns the answer space into exactly the F F F T T T predicate line from Section 1 โ and we hunt the boundary.
Answer value: lo ............................. hi
feasible()? F F F F T T T T T T T T
โ
smallest feasible answer = what we return
3.2 Framework & Templateโ
A reliable four-step recipe:
- Confirm monotonicity. Ask: "If answer
mis feasible, ism+1always feasible too?" If yes (or the mirror for maximization), proceed. This is the make-or-break step. - Set bounds
[lo, hi]of the answer space. Makelothe smallest conceivable answer andhithe largest. A too-wide range only costs a few extralogiterations โ err wide rather than risk excluding the answer. - Write
feasible(mid)โ a predicate returningTrue/Falsefor a specific candidate answer. This is where the real problem lives (greedy simulation, counting, etc.). - Binary search the boundary with the half-open pattern.
Generic reusable template โ minimization (find the smallest feasible answer):
def search_on_answer_min(lo, hi, feasible):
"""Return the smallest value in [lo, hi] for which feasible() is True.
Precondition: feasible is monotonic โ F...F T...T (once False turns True,
it stays True). Assumes an answer exists in [lo, hi].
"""
while lo < hi:
mid = lo + (hi - lo) // 2
if feasible(mid):
hi = mid # mid works โ it might be the best; keep it, search left
else:
lo = mid + 1 # mid fails โ answer must be larger
return lo # lo == hi == smallest feasible answer
Mirror template โ maximization (find the largest feasible answer, predicate T...T F...F):
def search_on_answer_max(lo, hi, feasible):
"""Return the largest value in [lo, hi] for which feasible() is True.
Precondition: feasible is monotonic โ T...T F...F.
"""
while lo < hi:
mid = lo + (hi - lo + 1) // 2 # ceil: bias mid UP to avoid infinite loop
if feasible(mid):
lo = mid # mid works โ try to go higher; keep it
else:
hi = mid - 1 # mid fails โ answer must be smaller
return lo
The single most important insight here: in the maximization template you must round
midup (+ 1before dividing). Otherwise, whenhi == lo + 1andfeasible(lo)is true,midrounds down tolo, you setlo = mid == lo, and the loop spins forever. The two templates are mirror images โ memorize the pair together, including which one uses the ceiling midpoint.
3.3 Worked Problemsโ
Problem A โ Koko Eating Bananas (LeetCode 875)โ
Koko has
pilesof bananas andhhours before the guards return. Each hour she picks one pile and eats up tokbananas from it (if the pile has fewer, she finishes it and stops for that hour). Find the minimum eating speedksuch that she finishes all bananas withinhhours.
Reasoning through the lenses:
- Beginner trap: trying to derive
kwith a formula from totals โ but theceilper pile makes a closed form messy. - Expert move: recognize that higher speed is always at least as feasible (eating faster never makes you finish later) โ monotonic โ search on answer.
Framework:
- Monotonic? If speed
kfinishes in time, so does anyk' > k. โ - Bounds:
lo = 1(must eat something),hi = max(piles)(eating faster than the biggest pile per hour gives no further benefit โ one pile per hour is the cap). feasible(k)= total hoursฮฃ ceil(pile / k) <= h.- Search for the smallest feasible
k.
import math
def min_eating_speed(piles, h):
def feasible(k):
return sum(math.ceil(p / k) for p in piles) <= h
lo, hi = 1, max(piles)
while lo < hi:
mid = lo + (hi - lo) // 2
if feasible(mid):
hi = mid
else:
lo = mid + 1
return lo
Verified: min_eating_speed([3,6,7,11], 8) == 4; min_eating_speed([30,11,23,4,20], 5) == 30; min_eating_speed([30,11,23,4,20], 6) == 23.
Complexity: O(n log M) where n = len(piles), M = max(piles). The log M is the binary search; each feasible call is O(n).
Problem B โ Capacity to Ship Packages within D Days (LeetCode 1011)โ
Given package
weightson a conveyor belt (must ship in order) andDdays, find the minimum ship capacity so all packages ship withinDdays. Each day you load consecutive packages without exceeding capacity.
This is exactly the guide's example: weights = [3,2,2,4,1,4], D = 3.
Framework:
- Monotonic? A bigger ship can carry anything a smaller ship can โ if capacity
Cworks, so doesC+1. โ - Bounds:
lo = max(weights)(the ship must at least hold the heaviest single package, since packages can't be split),hi = sum(weights)(one giant day carries everything). This bound choice is itself an expert detail โ settinglobelowmax(weights)makesfeasiblenever true there and wastes iterations, or worse, breaks a naive predicate. feasible(cap)= greedily pack days; the required days<= D.- Smallest feasible capacity.
def ship_within_days(weights, days):
def feasible(cap):
d, cur = 1, 0
for w in weights:
if cur + w > cap: # can't fit โ start a new day
d += 1
cur = 0
cur += w
return d <= days
lo, hi = max(weights), sum(weights)
while lo < hi:
mid = lo + (hi - lo) // 2
if feasible(mid):
hi = mid
else:
lo = mid + 1
return lo
Verified: ship_within_days([3,2,2,4,1,4], 3) == 6; ship_within_days([1,2,...,10], 5) == 15.
Trace of the answer 6 for [3,2,2,4,1,4], D=3: Day 1 = [3,2] โ 5 (adding 2 more would give 7 > 6? 5+2=7>6 so stop... actually 3+2=5, next +2=7>6 โ new day). Day 1 [3,2], Day 2 [2,4], Day 3 [1,4] โ 3 days. โ
Capacity 5 would need 4 days, so 6 is minimal.
Bonus โ the family this pattern unlocksโ
The exact same skeleton (lo = max, hi = sum, greedy feasible) solves Split Array Largest Sum (LC 410): partition nums into k subarrays minimizing the largest subarray sum.
def split_array(nums, k):
def feasible(limit):
cnt, cur = 1, 0
for n in nums:
if cur + n > limit:
cnt += 1
cur = 0
cur += n
return cnt <= k # can we do it in <= k parts under this limit?
lo, hi = max(nums), sum(nums)
while lo < hi:
mid = lo + (hi - lo) // 2
if feasible(mid):
hi = mid
else:
lo = mid + 1
return lo
Verified: split_array([7,2,5,10,8], 2) == 18. Notice Koko, Ship-Packages, and Split-Array are the same problem in three costumes โ spotting that is the expert skill.
Binary search over the reals (floats)โ
When the answer is continuous (e.g., sqrt, minimizing a convex cost), replace "shrink to one integer" with "iterate a fixed number of times" or "until hi - lo < eps":
def sqrt_bs(x, eps=1e-9):
lo, hi = 0.0, max(1.0, x)
for _ in range(100): # 100 halvings โ ~1e-30 precision; no infinite-loop risk
mid = (lo + hi) / 2
if mid * mid < x:
lo = mid
else:
hi = mid
return lo
Verified: sqrt_bs(2) โ 1.414213562. Expert tip: prefer a fixed iteration count over while hi - lo > eps for floats โ it sidesteps precision-induced infinite loops and gives predictable runtime.
4. Complexity Analysisโ
| Algorithm | Time | Space | Notes |
|---|---|---|---|
| Classic binary search (iterative) | O(log n) | O(1) | n = array length |
| Classic binary search (recursive) | O(log n) | O(log n) | stack frames |
| Lower / upper bound | O(log n) | O(1) | same as classic |
| Search on answer | O(C ยท log R) | O(1) extra | R = size of answer range (hi โ lo), C = cost of one feasible() call |
| Search on answer over reals | O(C ยท log((hiโlo)/eps)) or O(C ยท iters) | O(1) | fixed-iteration form is O(C ยท iters) |
Reading the search-on-answer cost. People forget the C factor. In Koko, C = O(n) (summing over piles) and R = max(piles), so total is O(n log(max(piles))). The binary search contributes only the logarithm of the value range โ cheap even for huge numeric ranges (searching [1, 10โน] is ~30 iterations).
Why the answer range's magnitude, not its element count, drives the log. Search on answer often ranges over 10โน or more possible values. Because we halve the value interval, the iteration count is logโ(hi โ lo) โ around 30โ60 even for astronomically large ranges. That's the whole reason the technique scales.
5. Applications in DS / AI / ML / LLMsโ
Data Structuresโ
- Binary Search Trees (BSTs). A BST is binary search made persistent in pointers: each node's left/right split embodies the same "discard half" decision, giving
O(log n)lookup on a balanced tree (AVL, Red-Black). - Sorted arrays &
bisect. Maintaining a sorted list and usinglower_bound/upper_boundfor insertion, range counts, and predecessor/successor queries. - Segment trees / Fenwick trees. "Binary search on the tree" โ descend the segment tree in
O(log n)to find, e.g., the k-th element or the first prefix-sum exceeding a threshold. - Answer-space in graphs. "Minimum maximum edge weight path", "minimize the largest distance" โ binary search the threshold, then run a linear/BFS feasibility check.
AI / MLโ
- Hyperparameter tuning. When a metric is monotonic in one hyperparameter (e.g., regularization strength vs. a constraint being satisfied, or model size vs. a latency budget), binary search finds the tightest setting far faster than grid search. Caveat: only valid where monotonicity truly holds โ many hyperparameters are non-monotonic, and there binary search is the wrong tool (use grid/random/Bayesian search).
- Classification threshold selection. The precisionโrecall tradeoff is monotonic in the decision threshold: raising the threshold monotonically increases precision and decreases recall. Binary-search the threshold to hit a target precision (e.g., "smallest threshold with precision โฅ 0.95") โ a textbook search-on-answer.
- Learning-rate / step-size line search. Backtracking line search and bisection line search locate a step size satisfying the Armijo/Wolfe conditions by halving an interval.
- Quantile & calibration lookups. Finding where a value falls in a sorted array of empirical quantiles (isotonic calibration, conformal prediction thresholds) is a direct
bisect.
LLMsโ
- Top-k / nucleus (top-p) sampling. After sorting token probabilities descending and computing the cumulative distribution, nucleus sampling binary-searches the cumulative sum for the smallest prefix whose mass โฅ
p. The CDF is monotonic โlower_boundon the cumulative array picks the nucleus cutoff inO(log V)over vocabularyV. - Sampling a token from a CDF. Drawing
u ~ Uniform(0,1)and finding the token viabisecton the cumulative probabilities is the standardO(log V)inverse-CDF sample. - KV-cache & positional lookups. Locating a position/segment within sorted cache offsets, or paged-attention block boundaries, via binary search.
- Beam search pruning thresholds. When pruning hypotheses by a score cutoff to retain a target beam size, binary-searching the score threshold over a sorted score array selects the cutoff efficiently.
- Context-window / batch-size fitting. "Largest batch size (or sequence length) that fits in GPU memory / latency budget" is search-on-answer: memory use is monotonic in batch size,
feasible(b)= "fits and meets SLA", binary-search the max feasibleb. (Same shape as Ship-Packages.)
Systemsโ
- Database & storage indexing. B-tree/B+-tree indexes are disk-friendly generalizations of binary search; within a sorted index page, lookups are binary search.
- Vector stores & embeddings. Exact nearest-neighbor over a 1-D projection, or locating a scalar (e.g., a norm or a cluster boundary) in sorted embedding metadata, uses binary search. (Note: high-dimensional ANN like HNSW/IVF is not binary search โ dimensionality breaks the total order binary search needs.)
- Rate limiting, versioning, log search. Finding the first log entry after a timestamp, the first failing commit (
git bisectโ literally binary search on answer over commit history), or the first version where a flag flips.
git bisectis the most beloved real-world search-on-answer: "find the first commit where the test fails" is a monotonicF...F T...Tpredicate over the commit timeline, and it finds the culprit inlog(commits)checkouts.
6. Expert Takeaways & Mental Modelsโ
5+ insights beginners missโ
- It's about a monotonic predicate, not a sorted array. Reframe every candidate problem as "is there a
F...F T...Tboundary?" If yes, binary search applies โ even with no array in sight. - Pick ONE convention and never mix. Either inclusive
[lo, hi]withwhile lo <= hi, or half-open[lo, hi)withwhile lo < hi. Most boundary bugs come from mixing the two. Professionals standardize on half-open forlower/upper bound. mid = lo + (hi - lo) // 2always. Even in Python where overflow can't happen, it's muscle memory that saves you in C++/Java/Rust. And in the maximization template, use the ceiling midpointlo + (hi - lo + 1) // 2to prevent infinite loops.- The hard part of search-on-answer is
feasible(), not the search. The binary search is 6 boilerplate lines. Your real work is designing a correct, cheap, monotonic feasibility predicate (often a greedy sweep or a count). - Bound generously. An answer range that's 10ร too wide costs only ~3โ4 extra iterations (
logโ 10 โ 3.3). An answer range that's too narrow and excludes the true answer is a silent correctness bug. When unsure, widenhi. - Binary search cannot validate its own precondition. On non-monotonic input it returns a plausible-looking wrong answer with no error. You are responsible for proving monotonicity.
- Return
loafter a[lo, hi)loop. Whenlo == hi, that index/value is the boundary. Don't second-guess with extra comparisons. - Prefer the language's battle-tested primitives. Use
bisect_left/bisect_right(Python),std::lower_bound/upper_bound(C++),Arrays.binarySearch(Java) in production โ but know the template cold for interviews and answer-space problems where no primitive fits.
Decision flowchart โ "Should I use binary search here?"โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Do I need to FIND a value, or the โ
โ MIN/MAX value satisfying a condition? โ
โโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโ
โ โ
FIND a value MIN / MAX under a condition
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโ โโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Is the data sorted / โ โ Can I write feasible(x) that โ
โ can I sort it once? โ โ cheaply tests one candidate? โ
โโโโโโโโโโโโฌโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ โ
โโโโโโดโโโโโโ โโโโโโโโโโดโโโโโโโโโ
YES NO YES NO
โ โ โ โ
โผ โผ โผ โผ
โโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โ Classic โ โ Is there โ โ Is feasible(x) โ โ Not a binary โ
โ binary โ โ hidden โ โ MONOTONIC in x? โ โ search โ
โ search / โ โ monotonic โ โ (F..F T..T pattern)โ โ problem. โ
โ bisect โ โ structure? โ โโโโโโโโโโโฌโโโโโโโโโโโ โ Use another โ
โโโโโโโโโโโโ โโโโโโโโฌโโโโโโโ โ โ technique โ
โ โโโโโโโโดโโโโโโโ โ (DP, greedy, โ
โโโโโโดโโโโโ YES NO โ heap, hash) โ
YES NO โ โ โโโโโโโโโโโโโโโโ
โ โ โผ โผ
โผ โผ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโ โโโโโโ โ SEARCH ON ANSWER โ โ Not binary โ
โ Binary โ โ No โ โ min: keep-left โ โ search โ โ
โ search on โ โ โ โ max: keep-right + โ โ rethink the โ
โ that โ โ โ โ ceil midpoint โ โ predicate โ
โ structure โ โโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโ
The universal template (covers ~95% of problems)โ
Everything reduces to "find the boundary of a monotonic predicate." Memorize this pair:
# MINIMIZATION: smallest x with feasible(x) == True (predicate: F F F T T T)
def find_min(lo, hi, feasible):
while lo < hi:
mid = lo + (hi - lo) // 2 # floor
if feasible(mid):
hi = mid # keep mid, look left for something smaller
else:
lo = mid + 1
return lo
# MAXIMIZATION: largest x with feasible(x) == True (predicate: T T T F F F)
def find_max(lo, hi, feasible):
while lo < hi:
mid = lo + (hi - lo + 1) // 2 # CEIL โ prevents infinite loop
if feasible(mid):
lo = mid # keep mid, look right for something larger
else:
hi = mid - 1
return lo
For classic exact search, feasible(i) = (arr[i] >= target) turns find_min into lower_bound. One mental model, every problem.
7. Quick Reference Cheat Sheetโ
Conventionsโ
| Convention | Loop | Init hi | Use for |
|---|---|---|---|
Inclusive [lo, hi] | while lo <= hi | len(arr) - 1 | exact-match search |
Half-open [lo, hi) | while lo < hi | len(arr) | lower/upper bound, boundary hunts |
Midpointโ
| Goal | Formula | Why |
|---|---|---|
| Minimization / keep-left | mid = lo + (hi - lo) // 2 | floor; pairs with hi = mid |
| Maximization / keep-right | mid = lo + (hi - lo + 1) // 2 | ceil; prevents infinite loop with lo = mid |
The two moves that must pair correctlyโ
| If a branch doesโฆ | The other branch must doโฆ | Guarantees |
|---|---|---|
hi = mid (keep candidate) | lo = mid + 1 | strict shrink, no infinite loop |
lo = mid (keep candidate) | hi = mid - 1 and use ceil midpoint | strict shrink, no infinite loop |
bisect mapping (Python)โ
| Task | bisect | Template |
|---|---|---|
First index โฅ target | bisect_left(a, x) | lower_bound |
First index > target | bisect_right(a, x) | upper_bound |
Count of x | bisect_right(a,x) - bisect_left(a,x) | upper โ lower |
| Insertion point (stay sorted) | bisect_left(a, x) | lower_bound |
Search-on-answer checklistโ
- โ Problem asks for a min/max value.
- โ Verifying a candidate is easier than computing the optimum.
- โ
feasible(x)is monotonic (FโฆF TโฆTorTโฆT FโฆF). - โ
lo= smallest possible answer,hi= largest possible answer (bound generously). - โ Minimize โ
find_min(floor mid,hi = mid). Maximize โfind_max(ceil mid,lo = mid). - โ Cost =
O(feasible ร log(hi โ lo)).
Complexity one-linersโ
- Classic:
O(log n)time,O(1)space (iterative). - Search on answer:
O(C ยท log R),C= feasibility cost,R= answer-range size. - Recursion adds
O(log n)stack space.
Canonical problem catalogโ
| Problem | Type | lo โฆ hi | feasible |
|---|---|---|---|
| Find element in sorted array | Classic | 0 โฆ n-1 | arr[mid] == target |
| First/last occurrence | Lower/Upper bound | 0 โฆ n | arr[mid] โฅ / > target |
| Koko Eating Bananas (LC 875) | Answer (min) | 1 โฆ max(piles) | ฮฃ ceil(p/mid) โค h |
| Ship Packages in D Days (LC 1011) | Answer (min) | max(w) โฆ sum(w) | greedy days โค D |
| Split Array Largest Sum (LC 410) | Answer (min) | max(w) โฆ sum(w) | greedy parts โค k |
| Sqrt / real optimization | Answer (float) | 0 โฆ x | fixed iterations |
git bisect | Answer (min) | first โฆ last commit | test passes? |
End of guide. Every code block above was executed and verified before inclusion.
Related Guidesโ
Prerequisites: Arrays & Strings ยท Big-O Notation & Complexity Analysis
See also: Sorting Algorithms ยท Two Pointers
Section: Core DSA ยท All guides