Binary Search,
discovered — not memorized
You already know how binary search works. The problem is everything the tutorial skips: knowing binary search applies, designing the search space, crafting the predicate, maintaining the invariant, and choosing the right template — on a problem you've never seen. This page teaches that missing middle.
How to actually use this page
This is long on purpose — it's a course, not a cheat sheet. Read it like this:
Fight the widgets
The interactive visualizer lets you choose the wrong boundary move and watch binary search fail. Do that first — losing on purpose is how the correct move earns its place.
Guess before revealing
Every purple "pause & think" box is a real question. Commit to an answer. Reveals only help once you've tried.
Live in Parts III–V
The invariant framework and the predicate design process are the reusable skill. Everything else is practice for them.
The map
Front-loaded toward thinking & invariants, back-loaded with patterns & reference. Jump around with the rail on the right, but the first read should be in order.
Mindset
Before any code: what binary search is at the deepest level, and why a ten-line algorithm can be the hardest thing on the page. Fix the mental model here and every template later feels inevitable instead of arbitrary.
01 · The Binary Search Mindset
Forget while (left < right) for a minute. That's the mechanism, not the reason. Here's what binary search actually is:
left and right boundaries make about where the answer lives.
Everything else — mid, boundary updates, answer storage — is just machinery for preserving the invariant while halving the search space. Once you believe binary search is "maintain an invariant while shrinking a monotonic space," the mystique inverts: the code was never the hard part. The hard part is designing the right invariant and the right monotonic property.
The three questions that define every binary search
What is my search space?
What am I searching over? Indices? Values? A range of possible answers? What are the absolute minimum and maximum this could be?
What is the monotonic property?
What yes/no question, asked about mid, produces a pattern like NNNNNYYYYY or YYYYYNNNNN across the search space? This is the predicate.
What is my invariant?
What promise do left and right make at the start of every iteration? E.g., "the answer is in [left, right]" or "everything left of left is invalid."
You're looking up a word in a physical dictionary. You open it somewhere in the middle. If the page is alphabetically before your word, you know — with absolute certainty — that the entire left half is useless. You don't "guess" the word is in the right half. You prove it, using the monotonic property of alphabetical order. That proof is the invariant. Every binary search is this game, played on a different kind of "alphabetical order."
The deep structure: monotonicity is everything
Binary search doesn't require a sorted array. It requires a monotonic predicate — a function that, as you sweep across your search space, transitions from false to true (or true to false) exactly once. A sorted array is just the most common way to produce such a predicate.
The FFFFTTTT pattern
If check(x) returns false for all x < k and true for all x ≥ k, then binary search can find k. The transition point is the answer. This is why "binary search on answer" works.
The FTFTFTFT pattern
If check(x) alternates, binary search can't work — there's no "half to discard." This is why you can't blindly binary search on unsorted data or arbitrary functions.
Pause & think: You have an array [3, 5, 8, 1, 2, 4, 6, 7]. Can you binary search for the value 4? Why or why not? reveal
Not directly — the array isn't sorted, so the predicate "is arr[mid] ≥ target?" isn't monotonic. At mid=3 (value 1), the answer is "no, go right," but the target 4 is actually at index 5, which IS to the right. However, at mid=2 (value 8), the answer is "yes, go left," but 4 is to the right. The predicate oscillates. But — you could binary search for the minimum in this rotated-sorted array (it's a rotated version of a sorted array), because "is arr[i] < arr[0]?" IS monotonic here. The monotonic property isn't "sorted" — it's something subtler that happens to exist in this structure.
02 · Why you actually struggle
You are not bad at binary search. You're missing the things editorials delete. Let's name the specific gaps.
The code is short, so the thinking is invisible
A binary search is 10–15 lines. The difficulty isn't in the code — it's in the design decisions before the code: what's the search space, what's the predicate, what's the invariant. Editorials show the code and skip the thinking, so it looks like the answer "just appeared." Fix: we make the design process visible and central (Parts III–V).
Too many templates look plausible
left < right vs left <= right? mid = left + (right-left)/2 vs mid = right - (right-left)/2? left = mid vs left = mid + 1? Each feels defensible, and the editorial just asserts one. Fix: every template choice is derived from the invariant, not memorized (Part IV).
Off-by-one errors feel random
You get left = mid + 1 right most of the time, but sometimes it should be left = mid. You return left but sometimes it should be left - 1. These aren't random — they're consequences of your invariant. Fix: once you state your invariant precisely, the ±1 decisions become mechanical (Part III).
"How did they know to binary search here?"
The editorial says "binary search on answer" and it looks like magic. It isn't: the author noticed the problem asks for a minimum feasible value, which implies a monotonic feasibility function. Fix: Part II teaches recognition through clue words and structural signals.
Confusing "I know binary search" with "I can use binary search"
You can implement Arrays.binarySearch(). That's like knowing Arrays.sort() and claiming you can design a scheduling algorithm. The real skill is applying binary search to novel structures. Fix: this entire page.
"How did anyone think to binary search here?" almost always has the same answer: they noticed the problem asks for an extremum (min/max/first/last) of something, and the feasibility/predicate is monotonic. You already know how to check feasibility. So you're much closer than you feel — you're one disciplined procedure away.
Recognizing Binary Search
Before you design anything, you need to suspect binary search in the first place — and, just as important, know when to reject it.
03 · When to suspect binary search — and when to run
Binary search and brute-force linear scan answer the same kinds of questions (search, optimize, find boundary). The difference is whether there's a monotonic property you can exploit. Run this decision flow:
Does the problem ask for a specific position (find, first, last), an extremum (minimum, maximum, smallest, largest), or a threshold (earliest, latest, feasible)?
Can you define a yes/no question check(x) such that the answers form a pattern like NNNNNYYYYY or YYYYYNNNNN as x increases across some range?
The clue words — your early-warning system
These words in a problem statement are strong signals that binary search might apply. They're not guarantees — but they should make you immediately ask "what's the monotonic property?"
Minimum / Maximum / Smallest / Largest
"Find the minimum capacity to ship all packages in D days" → binary search on the answer. The predicate: "can we ship with capacity mid?" is monotonic.
First / Last / Earliest / Latest
"Find the first bad version" → binary search for the transition from good→bad. "Find the last day before all flowers bloom" → binary search for a threshold.
Is it possible / Can we / Feasible
"Is it possible to divide the array into k subarrays with max sum ≤ mid?" → binary search on the max sum. Feasibility is almost always monotonic.
Optimize / Minimize the maximum / Maximize the minimum
"Maximize the minimum distance between balls" → binary search on the minimum distance. "Minimize the maximum" → binary search on the maximum. Both are monotonic.
① The problem asks for a min/max of something that's feasibility-monotonic (if it works for X, it works for all X' > X, or vice versa). ② The input has a sorted or rotated-sorted structure. ③ The answer space is a continuous range you can bisect. ④ Constraints are large (n ≤ 10⁹) but a feasibility check is cheap — O(n) check × O(log n) search = O(n log n), which fits.
① The predicate is not monotonic — you can build a counterexample where check(5) is true but check(6) is false and check(7) is true again. ② The problem asks for all solutions or counting (not an extremum). ③ The answer depends on combinatorial choices (which items to pick) rather than a single numeric parameter. ④ There's no ordered search space to bisect — the domain is a graph, a tree, or an unordered set.
The confusion table — binary search vs its look-alikes
| If the problem… | Reach for | Tell / why not binary search |
|---|---|---|
| asks for min/max of a monotonic feasibility function | Binary search on answer | The feasibility check creates the FFFFTTTT pattern. |
| has a sorted array and asks for a position | Standard binary search | The sorted order IS the monotonic property. |
| asks for all pairs / all subsets / count of something | Two pointers / Hashing / DP | Binary search finds one transition point, not all solutions. |
| optimizes over choices with overlapping subproblems | Dynamic programming | No single monotonic parameter to bisect — you need to compare futures. |
| processes elements in sorted order with a local commitment | Greedy | The sort enables greedy, not binary search. (Though greedy inside BS on answer is common.) |
| asks shortest path / connectivity / traversal | BFS / Dijkstra / DFS | The structure is a graph, not a searchable range. |
Pause & think: "Given an array of positions, place m balls such that the minimum distance between any two balls is maximized." Binary search? reveal
Yes — binary search on the answer. The problem asks to maximize the minimum distance. Define check(d) = "can we place m balls such that every pair is at least d apart?" This is monotonic: if we can place them with distance d, we can also place them with any distance d' < d. So the answers form NNNNNYYYYY (or YYYYYNNNNN depending on direction). Binary search finds the transition — the maximum feasible d. The predicate is a simple greedy: sort positions, place the first ball at position[0], then greedily place each next ball at the first position ≥ last placed + d.
Invariants, Search Spaces, Predicates & Boundaries
This is the heart of the page. Every binary search bug you've ever had traces back to a mistake in one of these four design decisions. Master them and the code writes itself.
04 · Loop Invariants — the one idea that prevents every bug
A loop invariant is a statement that is true at the start of every iteration of your loop — before the loop body runs, after it runs, and when the loop terminates. It's the contract your variables make. If you can state your invariant precisely, every boundary update, every return value, and every edge case becomes mechanical.
The fundamental binary search invariant:
"The answer, if it exists, is in the range [left, right]."
At the start of every iteration, you promise that the answer lives between left and right (inclusive). Every operation inside the loop must preserve this promise. When the loop ends, the invariant + the termination condition together tell you exactly where the answer is.
Why invariants prevent bugs
Every common binary search bug is an invariant violation:
left but the answer is at left - 1. Why: your invariant was "everything left of left is invalid" but you forgot that left itself might be invalid when the loop ends.left == right, the answer IS left. If your invariant is "everything < left is invalid, everything ≥ right is valid," then the answer is right (or left, depending on what you're finding). The invariant determines the return.left = mid when mid == left → no progress. Why: you failed to shrink the search space, violating the implicit invariant that "each iteration strictly reduces the range."left = mid + 1 (always shrinks), or use mid = left + (right - left + 1) / 2 (rounds up, so mid > left when left < right). The invariant + rounding choice must guarantee progress.Interactive: Watch the invariant in action
The widget below shows binary search on a sorted array. At each step, the invariant "the answer is in [left, right]" is maintained. Watch how the search space shrinks while the invariant holds.
Pause & think: In the visualizer above, when arr[mid] < target, we set left = mid + 1. What invariant does this preserve? reveal
The invariant is "the answer, if it exists, is in [left, right]." Since arr[mid] < target and the array is sorted, we know with certainty that arr[mid] is NOT the target and nothing at index ≤ mid can be the target (they're all ≤ arr[mid] < target). So we can safely exclude everything up to and including mid: the new invariant becomes "the answer is in [mid+1, right]." Setting left = mid + 1 preserves the invariant. The boundary move is a direct consequence of the invariant, not a memorized rule.
05 · Search Space Design
The search space is the set of all possible answers you're searching over. Getting this wrong means binary search runs on the wrong domain — like looking for a word in the wrong dictionary.
The three types of search spaces
Indices of an array
left = 0, right = n - 1. You're searching for a position in the array. Classic: find target in sorted array, find first/last occurrence, find peak element.
Values in a range
left = min_possible, right = max_possible. You're searching for a value that satisfies some property. Classic: binary search on answer — find the minimum capacity, maximum distance, etc.
Real numbers (continuous)
left = lo, right = hi, iterate until right - left < epsilon. You're searching for a real-valued answer. Classic: find the square root, find the optimal real-valued parameter.
How to determine your bounds
Ask: "What is the absolute smallest the answer could be?"
This is your left. For indices: 0. For "minimum capacity": the minimum possible capacity (often the max single element). For "maximum distance": 0 or 1.
Ask: "What is the absolute largest the answer could be?"
This is your right. For indices: n-1. For "minimum capacity": the sum of all elements (worst case: carry everything in one trip). For "maximum distance": max - min of the array.
Ask: "Is every value in [left, right] a valid candidate?"
If not, you need to rethink. The beauty of binary search on answer is that even if most values in [left, right] aren't "real" answers, the predicate still creates a monotonic FFFFTTTT pattern over them, and binary search finds the transition.
Problem: "Find the minimum speed to arrive on time." You set left = 1, right = max_distance. But the answer could be larger than any single distance (if time is very tight). Fix: think about the absolute worst case. If you need to cover distance d in time t < 1, speed must be > d/t. Set right to a safe upper bound like 10^7 or compute it analytically.
06 · Predicate Design — the most important skill
The predicate is the function boolean check(int mid) (or isPossible, canDo, feasible) that answers a yes/no question about mid. This is the single most important design decision in any binary search problem. Get the predicate right and the rest is mechanical. Get it wrong and nothing else matters.
The predicate design process
Identify the parameter
What is mid representing? An index? A capacity? A distance? A speed?
State the question
"Given that mid is the answer, can I verify it in O(n) or O(n log n)?"
Write the check
Implement the verification. This is often a greedy simulation, a counting argument, or a simple comparison.
Prove monotonicity
If check(x) is true, is check(x+1) also true (or also false)? If not, your predicate is wrong.
Example: Koko Eating Bananas
Problem: Koko has n piles of bananas. She has h hours. She eats k bananas per hour from a pile (moving to the next pile if she finishes early). Find the minimum k such that she finishes all piles in ≤ h hours.
What is the search space?
We're searching for k — the eating speed. left = 1 (minimum possible speed), right = max(piles) (at this speed, every pile takes ≤ 1 hour, so total hours = n ≤ h is guaranteed if a solution exists).
What is the predicate?
check(k) = "can Koko finish all piles in ≤ h hours at speed k?" To verify: for each pile of size p, she needs ceil(p / k) hours. Sum all these up. If total ≤ h, return true.
Why is it monotonic?
If Koko can finish at speed k, she can definitely finish at speed k+1 (she eats faster, so each pile takes ≤ as many hours). So check(k) = true → check(k+1) = true. The pattern is NNNNNYYYYY — we want the first Y, which is the minimum k.
// Predicate: can Koko finish at speed k?
boolean canFinish(int[] piles, int h, int k) {
int hours = 0;
for (int p : piles) {
hours += (p + k - 1) / k; // ceil(p/k) without floating point
}
return hours <= h;
}
// Binary search: find the minimum k where canFinish(k) is true
public int minEatingSpeed(int[] piles, int h) {
int left = 1, right = 0;
for (int p : piles) right = Math.max(right, p);
while (left < right) {
int mid = left + (right - left) / 2;
if (canFinish(piles, h, mid)) {
right = mid; // mid works, but maybe a smaller k also works
} else {
left = mid + 1; // mid is too slow, need faster
}
}
return left;
}
Pause & think: Why does right = mid (not mid - 1) when canFinish(mid) is true? reveal
Because mid itself might be the answer! We know canFinish(mid) is true, so the minimum valid k is ≤ mid. But it could BE mid. If we set right = mid - 1, we'd exclude mid from the search space — and if mid IS the minimum, we'd miss it. The invariant is "the answer is in [left, right]." Since mid is a valid candidate, the new invariant should be "the answer is in [left, mid]," so right = mid.
Testing your predicate
Before writing the binary search, always verify your predicate with these checks:
- Base case: Does
check(left_bound)give the expected result? Doescheck(right_bound)? - Monotonicity: Can you construct a case where
check(x)is true butcheck(x+1)is false (for a "find first true" problem)? If yes, your predicate is wrong. - Edge case: What happens at the extremes? If the answer is the minimum possible value, does your predicate correctly identify it?
Incorrect predicates — and why they fail
Seeing a wrong predicate is more instructive than seeing a right one. Here are two common mistakes:
Problem: split array into k subarrays, minimize the largest subarray sum. You search on the maximum allowed sum mid.
Wrong check: "can we split into EXACTLY k subarrays with max sum ≤ mid?"
Why it breaks monotonicity: If you require exactly k, then increasing mid might let you split into fewer than k parts (since each part can be bigger), so the count goes from true→false→true as mid grows. Not monotonic.
Right check: "can we split into AT MOST k subarrays with max sum ≤ mid?"
Why it works: If you can do it in ≤ k parts with max sum = mid, you can also do it in ≤ k parts with max sum = mid+1 (just allow bigger parts). Monotonic: false→true as mid increases. The minimum mid where this is true is the answer. "At most k" — not "exactly k." This is the #1 predicate mistake in partition problems.
Wrong check: "does Koko eat EXACTLY h hours at speed k?" — requiring an exact match on hours.
Why it breaks: The total hours at speed k is Σ⌈p/k⌉. This is a step function — it stays constant for a range of k, then drops. So "exactly h hours" is true at some k values and false at both smaller and larger k. Not monotonic.
Right check: "can Koko finish in ≤ h hours at speed k?" — using ≤ not ==.
Why it works: If she can finish in ≤ h hours at speed k, she can definitely finish in ≤ h hours at speed k+1 (faster eating = fewer hours). Monotonic: false→true. The first k where this is true is the minimum speed.
Almost every binary-search-on-answer predicate should use ≤ (at most, at least) or ≥, never == (exactly). Exact equality is almost never monotonic. "Can we do it in at most k?" is monotonic; "Can we do it in exactly k?" usually isn't. When you catch yourself writing == in a predicate, stop and switch to ≤ or ≥.
Interactive: Predicate Simulator
The widget below lets you build a predicate and see the resulting FFFFTTTT (or YYYYYNNNNN) pattern. Try to predict the pattern before revealing it.
07 · Boundary Movement — proved, not memorized
Every boundary update is a consequence of the invariant and the predicate. Never memorize left = mid + 1 — derive it.
The derivation process
When you evaluate check(mid), you learn something about mid. The boundary update encodes what you learned:
mid is a valid answer (or too small, depending on the problem)
"Find first true": mid could be the answer, but earlier positions might also be true. So: right = mid (keep mid in the search space, but exclude everything after mid).
"Find last false": mid is past the last false. So: right = mid - 1 (exclude mid, search earlier).
mid is not valid (or too large, depending on the problem)
"Find first true": mid is not true, so the first true must be after mid. So: left = mid + 1 (exclude mid and everything before).
"Find last false": mid could be the last false, but later positions might also be false. So: left = mid (keep mid, exclude everything before).
The golden rule: The boundary update must be consistent with your invariant. If your invariant says "the answer is in [left, right]," then:
- When you exclude mid, set the boundary to
mid ± 1 - When you keep mid as a candidate, set the boundary to
mid - Always ensure the search space strictly shrinks (otherwise infinite loop)
Interactive: Predict the boundary move
The widget below shows a binary search finding the first true in a FFFFTTTT pattern. Before stepping, predict which boundary will move and why.
Why mid = left + (right - left) / 2 and not (left + right) / 2?
Integer overflow. If left and right are both close to Integer.MAX_VALUE, their sum overflows to a negative number. left + (right - left) / 2 avoids this because right - left is always non-negative and bounded. Alternatively, in Java: (left + right) >>> 1 (unsigned right shift).
08 · Answer Storage — when to save mid and when not to
Some binary searches store answer = mid during the loop. Others don't. The difference is determined by your loop condition and invariant.
When using left <= right
The loop ends when left > right — the search space is empty. You can't just return left or right because neither is guaranteed to be the answer. Instead, save answer = mid every time you find a valid candidate. At the end, answer holds the best one found.
When using left < right
The loop ends when left == right. The invariant "the answer is in [left, right]" collapses to "the answer IS left (= right)." So you just return left. No need for an answer variable — the boundary itself is the answer.
Template A: Store answer
int ans = -1;
while (left <= right) {
int mid = left + (right-left)/2;
if (check(mid)) {
ans = mid; // save candidate
right = mid - 1; // look for better
} else {
left = mid + 1;
}
}
return ans;Template B: Return boundary
while (left < right) {
int mid = left + (right-left)/2;
if (check(mid)) {
right = mid; // mid is candidate
} else {
left = mid + 1; // mid is not
}
}
return left; // left == rightMixing the two templates. If your loop is left < right but you also store answer = mid — you have redundant logic and potential bugs. Pick ONE approach per problem: either the loop narrows to a single element (return boundary) or the loop empties the space (store answer). Don't do both.
Practice: Identify the template and invariant
For each problem below, determine: What is the search space? What is the invariant? Which template? Which boundary moves? Should I store the answer? What should I return? Don't reveal until you've committed.
Exercise 1: "Find the smallest divisor such that the sum of ⌈arr[i]/d⌉ ≤ threshold." reveal
Search space: d (the divisor). left = 1, right = max(arr) (at max divisor, each term is 1, sum = n, which is the smallest possible threshold — if threshold < n, no answer exists).
Predicate: check(d) = "sum of ⌈arr[i]/d⌉ ≤ threshold." If true, d is feasible.
Monotonicity: As d increases, each ⌈arr[i]/d⌉ decreases or stays the same, so the sum decreases. If check(d) is true, check(d+1) is also true. Pattern: NNNNNYYYYY.
Template: Template 5 (find minimum feasible = first true). while (left < right), right = mid when true, left = mid + 1 when false. Return left. No answer storage needed.
Exercise 2: "A sorted array was rotated. Find how many times it was rotated." reveal
Search space: Indices [0, n-1].
Invariant: "The index of the minimum element is in [left, right]."
Predicate: Compare arr[mid] with arr[right]. If arr[mid] > arr[right], the minimum is in the right half. If arr[mid] ≤ arr[right], the minimum is at mid or in the left half.
Template: Template 7 (rotated min). while (left < right), left = mid + 1 when arr[mid] > arr[right], right = mid otherwise. Return left (the index of the minimum = the number of rotations).
Exercise 3: "You have n jobs with processing times. Assign them to k workers to minimize the maximum total time any worker does." reveal
Search space: The maximum allowed time T per worker. left = max(jobs) (at minimum, one worker handles the longest job), right = sum(jobs) (one worker does everything).
Predicate: check(T) = "can we assign all jobs to ≤ k workers such that each worker's total ≤ T?" Greedy: accumulate jobs until exceeding T, then assign to a new worker. Count workers. If ≤ k, true.
Monotonicity: If we can do it with max time T, we can do it with T+1 (more room per worker = fewer or equal workers). Pattern: NNNNNYYYYY.
Template: Template 5 (find minimum feasible). right = mid when true, left = mid + 1 when false. Return left. Note: if jobs must be assigned contiguously, the greedy works. If jobs can be picked in any order, you need DP in the predicate — and this becomes a BS + DP problem (Family 11).
The Template Library
Templates are not for memorization — they're for understanding. Each template encodes a specific invariant. Once you see why each line exists, you'll never need to memorize again.
09 · Every Binary Search Template, Explained
Template 1: Standard Binary Search (find exact target)
Find exact value in sorted array
public int search(int[] arr, int target) {
int left = 0, right = arr.length - 1;
while (left <= right) { // search space is [left, right], inclusive
int mid = left + (right - left) / 2;
if (arr[mid] == target) return mid; // found it
else if (arr[mid] < target) left = mid + 1; // target is in [mid+1, right]
else right = mid - 1; // target is in [left, mid-1]
}
return -1; // search space empty → not found
}
Invariant: "If target exists, it's at an index in [left, right]."
Why left <= right: The search space includes both endpoints. When left == right, there's still one element to check.
Why mid ± 1: We checked arr[mid] and it's NOT the target, so we exclude it.
Why return -1: The loop ended because left > right — the search space is empty, so target doesn't exist.
Template 2: Lower Bound (first element ≥ target)
Find first position where arr[i] ≥ target
public int lowerBound(int[] arr, int target) {
int left = 0, right = arr.length; // right = n, NOT n-1: answer could be "past the end"
while (left < right) { // narrows to a single point
int mid = left + (right - left) / 2;
if (arr[mid] >= target) {
right = mid; // mid could be the answer; keep it
} else {
left = mid + 1; // mid is too small; exclude it
}
}
return left; // left == right == first index where arr[i] >= target
}
Invariant: "The first index where arr[i] ≥ target is in [left, right]."
Why right = arr.length: If all elements are < target, the answer is "past the end" (index n). We need n in the search space.
Why right = mid (not mid-1): arr[mid] ≥ target means mid IS a valid candidate for "first ≥ target." We can't exclude it.
Why return left: Loop ends when left == right. The invariant collapses to "the answer IS left."
Template 3: Upper Bound (first element > target)
Find first position where arr[i] > target
public int upperBound(int[] arr, int target) {
int left = 0, right = arr.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] > target) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
Only difference from lower bound: The predicate is arr[mid] > target instead of arr[mid] >= target. This finds the first element strictly greater than target. Used to compute the range of duplicates: [lowerBound, upperBound - 1].
Template 4: First Occurrence / Last Occurrence
Find first/last index of target in sorted array with duplicates
public int[] searchRange(int[] arr, int target) {
int first = lowerBound(arr, target);
if (first == arr.length || arr[first] != target) return new int[]{-1, -1};
int last = upperBound(arr, target) - 1;
return new int[]{first, last};
}
Why this works: lowerBound gives the first index ≥ target. If that index has value ≠ target, target doesn't exist. Otherwise, upperBound - 1 gives the last index == target (since upperBound is the first index > target).
Template 5: Binary Search on Answer (find minimum feasible)
Find the minimum value of x such that check(x) is true
public int findMinimumFeasible(int lo, int hi) {
int left = lo, right = hi;
while (left < right) {
int mid = left + (right - left) / 2;
if (check(mid)) {
right = mid; // mid works; maybe something smaller also works
} else {
left = mid + 1; // mid doesn't work; need something bigger
}
}
return left; // the first (minimum) value where check is true
}
Invariant: "The minimum feasible value is in [left, right]."
Monotonicity: check(x) = false for x < answer, check(x) = true for x ≥ answer.
Why right = mid: If check(mid) is true, mid is feasible. The minimum feasible is ≤ mid. We keep mid in the search space.
Why left = mid + 1: If check(mid) is false, mid is NOT feasible. The minimum feasible is > mid. We exclude mid.
Template 6: Binary Search on Answer (find maximum feasible)
Find the maximum value of x such that check(x) is true
public int findMaximumFeasible(int lo, int hi) {
int left = lo, right = hi;
while (left < right) {
int mid = left + (right - left + 1) / 2; // ROUND UP to avoid infinite loop
if (check(mid)) {
left = mid; // mid works; maybe something bigger also works
} else {
right = mid - 1; // mid doesn't work; need something smaller
}
}
return left; // the last (maximum) value where check is true
}
Why mid = left + (right - left + 1) / 2 (round up): When check(mid) is true and we set left = mid, we need to guarantee that mid > left (otherwise left doesn't change → infinite loop). Rounding up ensures mid ≥ left + 1 when left < right. This is the critical difference from Template 5.
If you use left = mid with mid = left + (right - left) / 2 (round down), then when right = left + 1, mid = left, and left = mid = left → no progress → infinite loop. Always pair left = mid with round-up, and right = mid with round-down.
Template 7: Rotated Sorted Array (find minimum)
Find the minimum in a rotated sorted array
public int findMin(int[] arr) {
int left = 0, right = arr.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] > arr[right]) {
left = mid + 1; // min is in the right half (past the pivot)
} else {
right = mid; // min is in the left half (mid could be the min)
}
}
return arr[left];
}
The monotonic property: In a rotated sorted array like [4,5,6,1,2,3], compare arr[mid] with arr[right]. If arr[mid] > arr[right], the pivot (minimum) must be to the right of mid (because in a properly sorted portion, mid ≤ right). If arr[mid] ≤ arr[right], the pivot is at mid or to the left.
Template 8: Peak Element
Find any peak element (greater than both neighbors)
public int findPeakElement(int[] arr) {
int left = 0, right = arr.length - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] > arr[mid + 1]) {
right = mid; // descending here → peak is at mid or to the left
} else {
left = mid + 1; // ascending here → peak is to the right
}
}
return left;
}
The monotonic property: Compare arr[mid] with arr[mid+1]. If ascending (arr[mid] < arr[mid+1]), a peak must exist to the right (the array must eventually descend). If descending, a peak exists at mid or to the left. This works even without a sorted array — it exploits the slope.
Using arr[mid] compared with arr[left] instead of arr[mid+1]. The slope is between mid and mid+1, not between mid and left. The wrong comparison gives no directional information about where a peak lives.
Template 9: Mountain Array (find peak, then search both sides)
Search in a bitonic (mountain) array
A mountain array increases to a peak, then decreases. To search for a target: (1) find the peak using Template 8, (2) binary search the ascending left side, (3) if not found, binary search the descending right side (with reversed comparisons).
public int findInMountainArray(int[] arr, int target) {
// Step 1: find the peak (Template 8)
int peak = findPeak(arr);
// Step 2: search the ascending left side [0, peak]
int idx = ascBinarySearch(arr, 0, peak, target);
if (idx != -1) return idx;
// Step 3: search the descending right side [peak+1, n-1]
return descBinarySearch(arr, peak + 1, arr.length - 1, target);
}
int ascBinarySearch(int[] arr, int lo, int hi, int target) {
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] == target) return mid;
else if (arr[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
int descBinarySearch(int[] arr, int lo, int hi, int target) {
while (lo <= hi) { // same structure, REVERSED comparisons
int mid = lo + (hi - lo) / 2;
if (arr[mid] == target) return mid;
else if (arr[mid] > target) lo = mid + 1; // reversed!
else hi = mid - 1;
}
return -1;
}
Forgetting to reverse the comparison in the descending search. On the right side of the mountain, larger values are to the left. If arr[mid] > target, the target is to the right (smaller values), so lo = mid + 1 — the opposite of the ascending case.
Template 10: Infinite / Unknown-Size Array (Exponential Search)
Search in a sorted array of unknown length
The array is sorted but you don't know its size (e.g., a stream or an ArrayReader interface). You can't set right = n-1 because you don't know n. Solution: exponentially grow a bound until you find an element ≥ target (or hit the end), then binary search within [bound/2, bound].
public int search(ArrayReader reader, int target) {
// Step 1: find the search bound by doubling
int hi = 1;
while (reader.get(hi) < target) {
hi *= 2; // exponential growth: 1, 2, 4, 8, 16, …
if (reader.get(hi) == Integer.MAX_VALUE) break; // past the end
}
int lo = hi / 2; // we know target > reader.get(hi/2)
// Step 2: standard binary search in [lo, hi]
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
int val = reader.get(mid);
if (val == target) return mid;
else if (val < target) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
Why exponential: Doubling the index finds a bound in O(log position) steps. Then binary search within the bound is also O(log position). Total: O(log position) — you never need to know n. This is called exponential search or galloping search.
Starting hi = 0 — then hi *= 2 never grows. Start at hi = 1. Also: handle the "out of bounds" case where reader.get(hi) returns a sentinel like MAX_VALUE — you must stop doubling and set hi to the last valid index.
Template 11: Matrix Binary Search (two variants)
Search in a 2D sorted matrix
Variant A — fully sorted matrix: Each row is sorted, and row[i][0] > row[i-1][last]. The entire matrix is a single sorted sequence if flattened. Map 1D index to 2D: row = idx / cols, col = idx % cols.
public boolean searchMatrix(int[][] mat, int target) {
int rows = mat.length, cols = mat[0].length;
int lo = 0, hi = rows * cols - 1; // treat as 1D sorted array
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
int val = mat[mid / cols][mid % cols]; // 1D → 2D mapping
if (val == target) return true;
else if (val < target) lo = mid + 1;
else hi = mid - 1;
}
return false;
}
Variant B — row & column sorted (staircase search): Rows are sorted left→right, columns top→bottom, but rows don't chain. Start at the top-right corner. If current > target, move left. If current < target, move down. Each step eliminates a row or column.
public boolean searchMatrixII(int[][] mat, int target) {
int row = 0, col = mat[0].length - 1; // start at top-right
while (row < mat.length && col >= 0) {
if (mat[row][col] == target) return true;
else if (mat[row][col] > target) col--; // too big → go left
else row++; // too small → go down
}
return false;
}
Why top-right: At the top-right corner, moving left gives smaller values and moving down gives larger values — it's a "decision point" like mid in 1D binary search. Starting at the bottom-left works too (reversed directions). Starting at top-left or bottom-right does not work — both directions give the same monotonicity, so you can't decide which way to go.
Starting at top-left (0,0) for the staircase search. From (0,0), both right and down give larger values — you can't decide which direction to move. You need a corner where one direction increases and the other decreases. That's top-right or bottom-left only.
Template 12: Continuous Binary Search (real-valued)
Binary search over real numbers
public double binarySearchReal(double lo, double hi) {
double left = lo, right = hi;
while (right - left > 1e-7) { // precision threshold
double mid = left + (right - left) / 2;
if (check(mid)) {
right = mid; // same logic as integer BS
} else {
left = mid; // NOTE: no +1 — reals have no gaps
}
}
return left; // or (left + right) / 2
}
Key differences from integer BS: ① Loop condition is right - left > epsilon, not left < right — reals never "equal." ② left = mid (not mid + 1) because there are infinitely many reals between mid and mid+1 — you can't "skip" one. ③ Choose epsilon based on required precision. For competitive programming, 1e-7 or 1e-9 is standard. Alternatively, run a fixed number of iterations (e.g., 100) — after 100 halvings, the range has shrunk by 2¹⁰⁰, which is more than enough precision.
Using left = mid + 1 in continuous BS. There are infinitely many reals between mid and mid+1 — skipping to mid+1 could skip the answer. Always use left = mid and right = mid. The epsilon loop condition guarantees termination because the range halves each step.
Example — Sqrt(x): Find √x to 5 decimal places. check(mid) = mid*mid > x. If true, sqrt is smaller → right = mid. If false, sqrt is larger → left = mid.
10 · Choosing the Right Template
Use this decision flowchart when you encounter a new problem:
Is the array sorted (or rotated sorted)?
Does the problem ask for a minimum or maximum of some value, where feasibility is monotonic?
The master comparison table
| Template | Search Space | Loop | Mid Rounding | When check(mid) true | When check(mid) false | Return |
|---|---|---|---|---|---|---|
| 1 · Exact search | [0, n-1] | left ≤ right | down | return mid | shrink both sides | -1 if not found |
| 2 · Lower bound | [0, n] | left < right | down | right = mid | left = mid + 1 | left |
| 3 · Upper bound | [0, n] | left < right | down | right = mid | left = mid + 1 | left |
| 4 · First/Last occ. | [0, n] | left < right | down | right = mid | left = mid + 1 | left / upper-1 |
| 5 · Min feasible | [lo, hi] | left < right | down | right = mid | left = mid + 1 | left |
| 6 · Max feasible | [lo, hi] | left < right | UP | left = mid | right = mid - 1 | left |
| 7 · Rotated min | [0, n-1] | left < right | down | right = mid | left = mid + 1 | arr[left] |
| 8 · Peak | [0, n-1] | left < right | down | right = mid | left = mid + 1 | left |
| 9 · Mountain | two phases | left ≤ right | down | return mid | shrink | index or -1 |
| 10 · Infinite | [hi/2, hi] | left ≤ right | down | return mid | shrink | index or -1 |
| 11 · Matrix A | [0, rows×cols-1] | left ≤ right | down | return true | shrink | boolean |
| 11 · Matrix B | staircase | row/col walk | n/a | return true | move left/down | boolean |
| 12 · Continuous | [lo, hi] real | hi-lo > ε | down | right = mid | left = mid | left |
If you set left = mid (keep mid), round UP (use right - left + 1). If you set right = mid (keep mid), round DOWN (standard). This guarantees progress and prevents infinite loops. That's the entire rule.
Building the Thought Process
This is the most important chapter. For every major pattern, we show the complete journey from problem statement to working binary search — including the wrong turns and the reasoning that eliminates them.
11 · The Complete Thinking Journey
Here is the exact sequence of questions an experienced solver asks when facing a new binary search problem. This is not a template — it's a thinking process.
Read & classify
What is the problem asking? Min/max? First/last? Exact position? Does it smell like BS?
Brute force
How would I solve this without BS? Linear scan? Try all values? This reveals the structure.
Find monotonicity
What property transitions from false→true (or true→false) as some parameter increases?
Define search space
What is the parameter? What are its absolute min and max? That's [left, right].
Write predicate
"Given parameter = mid, can I verify feasibility in O(n)?" Write check(mid).
Choose template
Min feasible? Max feasible? First true? Select the template and derive boundary moves from the invariant.
Code & verify
Write the BS. Test edge cases. Verify the invariant holds at every step.
Walkthrough: Aggressive Cows
Problem: Given n stalls at positions stalls[] and c cows, place all c cows in stalls such that the minimum distance between any two cows is maximized. Return this maximum minimum distance.
Step 1: Read & classify
"Maximize the minimum distance" — this is an extremum of a feasibility function. Strong BS-on-answer signal.
Step 2: Brute force
Sort the stalls. Try every possible minimum distance d from 0 to max-min. For each d, greedily check if we can place c cows. Return the largest d that works. This is O((max-min) × n). We can binary search the d.
Step 3: Find monotonicity
check(d) = "can we place c cows with minimum distance d?" If we can place them with distance d, we can definitely place them with distance d-1 (less restrictive). So: check(d) true → check(d-1) true. Pattern: YYYYYNNNNN. We want the last Y (maximum d where check is true).
Step 4: Define search space
left = 0 (minimum possible distance — two cows in adjacent stalls at the same position, or distance 0). right = stalls[n-1] - stalls[0] (maximum possible distance — only 2 cows, at the extremes). After sorting, of course.
Step 5: Write predicate
check(d): Sort stalls. Place first cow at stalls[0]. For each subsequent cow, place it at the first stall that is ≥ last_cow_position + d. If we can place all c cows, return true.
Step 6: Choose template
We want the maximum feasible d → Template 6 (find max feasible). Pattern: YYYYYNNNNN. Use left = mid when check(mid) is true, right = mid - 1 when false. Round UP for mid.
public int solve(int[] stalls, int c) {
Arrays.sort(stalls);
int n = stalls.length;
int left = 0, right = stalls[n-1] - stalls[0];
while (left < right) {
int mid = left + (right - left + 1) / 2; // round UP (Template 6)
if (canPlace(stalls, c, mid)) {
left = mid; // mid works; try bigger
} else {
right = mid - 1; // mid too big; try smaller
}
}
return left;
}
boolean canPlace(int[] stalls, int c, int minDist) {
int count = 1, lastPos = stalls[0];
for (int i = 1; i < stalls.length; i++) {
if (stalls[i] - lastPos >= minDist) {
count++;
lastPos = stalls[i];
if (count >= c) return true;
}
}
return count >= c;
}
Walkthrough: Find the Duplicate Number (Floyd's + BS)
Problem: Given an array of n+1 integers where each integer is in [1, n], there is exactly one duplicate (possibly repeated). Find it without modifying the array and O(1) extra space.
Step 1: Read & classify
Find a duplicate. The array isn't sorted. But the values are in [1, n]. Can we binary search on the VALUES?
Step 2: Key observation
For any value x in [1, n], count how many elements in the array are ≤ x. If there's no duplicate, exactly x elements should be ≤ x (pigeonhole). If more than x elements are ≤ x, the duplicate is in [1, x]. If exactly x elements are ≤ x, the duplicate is in [x+1, n].
Step 3: Monotonicity
check(x) = "count of elements ≤ x is > x". If true, the duplicate is in [1, x]. If false, the duplicate is in [x+1, n]. This creates a clean binary search on the value range [1, n].
Step 4-6: Search space and template
left = 1, right = n. We're finding the first x where the count exceeds x. Template 5 (find first true / minimum feasible).
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 num : nums) {
if (num <= mid) count++;
}
if (count > mid) {
right = mid; // duplicate is in [1, mid]
} else {
left = mid + 1; // duplicate is in [mid+1, n]
}
}
return left;
}
Pause & think: In the duplicate number problem, why does binary search on VALUES work even though the array isn't sorted? reveal
Because binary search doesn't require a sorted array — it requires a monotonic predicate. Our predicate is "count of elements ≤ mid is > mid." This is a function of the VALUE mid, not the array index. As mid increases from 1 to n, the count of elements ≤ mid can only increase (more values qualify). So the predicate "count > mid" transitions from false to true exactly once. The array's order is irrelevant — we're counting, not navigating.
Walkthrough: Search in Rotated Sorted Array
Problem: A sorted array was rotated at an unknown pivot. Search for a target value. Return its index, or -1.
Step 1: Read & classify
The array isn't fully sorted — it's rotated. But it has partial order: one half of any split is always sorted. This is a structural signal for binary search.
Step 2: Wrong first idea
My first instinct: find the pivot (the minimum), then do two separate binary searches on the two sorted halves. Problem: that's two passes. Can I do it in one?
Step 3: Key observation
At any mid, one of the two halves [left, mid] or [mid, right] is sorted. I can determine which half is sorted by comparing arr[mid] with arr[left] (or arr[right]). Then I check if the target is in the sorted half — if yes, search there; if no, search the other half.
Step 4: Monotonicity
The predicate isn't a simple true/false pattern. Instead, at each step I determine which half to discard based on where the target could be. The invariant: "the target, if it exists, is in [left, right]." Each step eliminates half the search space.
Step 5: The decision tree
If arr[mid] ≥ arr[left] → left half is sorted. If target is in [arr[left], arr[mid]) → go left; else → go right. Otherwise (right half is sorted): if target is in (arr[mid], arr[right]] → go right; else → go left.
public int search(int[] arr, int target) {
int left = 0, right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) return mid;
if (arr[left] <= arr[mid]) { // left half is sorted
if (arr[left] <= target && target < arr[mid]) right = mid - 1; // target is in left half
else left = mid + 1; // target is in right half
} else { // right half is sorted
if (arr[mid] < target && target <= arr[right]) left = mid + 1; // target is in right half
else right = mid - 1; // target is in left half
}
}
return -1;
}
Using arr[left] < arr[mid] (strict) instead of arr[left] ≤ arr[mid]. When left == mid (only 2 elements left), the left half has just one element — it IS sorted. The ≤ handles this. With <, you'd incorrectly classify the left half as unsorted when it has 1-2 elements.
Walkthrough: Capacity to Ship Packages
Problem: Ship packages of weights w[] in order within D days. Find the minimum capacity C such that you can ship all packages. Each day's total weight ≤ C.
Step 1: Read & classify
"Minimum capacity" — classic BS on answer. We're searching for the smallest C that works.
Step 2: Search space
left = max(w) — the minimum possible capacity (at minimum, you must carry the heaviest single package). right = sum(w) — the maximum (carry everything in one day). The answer is somewhere in between.
Step 3: Predicate
check(C) = "can we ship all packages in ≤ D days at capacity C?" Greedily: accumulate weights until adding the next package exceeds C, then start a new day. Count total days. If ≤ D, return true.
Step 4: Why is it monotonic?
If we can ship in ≤ D days at capacity C, we can also ship in ≤ D days at capacity C+1 (bigger capacity = more room per day = fewer or equal days). So check(C) true → check(C+1) true. Pattern: NNNNNYYYYY. We want the first Y = minimum C.
Step 5: Template selection
Template 5 (find minimum feasible). while (left < right), right = mid when check true, left = mid + 1 when false. Return left.
boolean canShip(int[] w, int days, int cap) {
int count = 1, load = 0; // start day 1, 0 load
for (int wt : w) {
if (load + wt > cap) { // new day needed
count++;
load = 0;
}
load += wt;
}
return count <= days;
}
public int shipWithinDays(int[] w, int days) {
int left = 0, right = 0;
for (int wt : w) { left = Math.max(left, wt); right += wt; }
while (left < right) {
int mid = left + (right - left) / 2;
if (canShip(w, days, mid)) right = mid;
else left = mid + 1;
}
return left;
}
Pause & think: Why is left = max(w) and not 1? What goes wrong if you use left = 1? reveal
If left = 1 but the heaviest package weighs 500, then check(1) through check(499) are all guaranteed false — you can never fit a 500-weight package into a capacity of 499. Binary search would still work (it'd just waste iterations), but you'd risk an error: if your predicate doesn't handle "impossible capacity" gracefully (e.g., if load + wt > cap and wt > cap, the day never gets started), you could get wrong answers or infinite loops. Setting left = max(w) ensures every candidate in the search space is at least physically possible — every package fits in a single day's capacity. Tighter bounds = fewer iterations + no impossible candidates.
The Pattern Library
Binary search problems cluster into families. Each family has a characteristic structure, a standard predicate, and common pitfalls. Learn the families and new problems become variations on a theme.
12 · Binary Search Pattern Families
Binary Search on Answer — Minimize the Maximum
Representative problems: Koko Eating Bananas, Capacity To Ship Packages, Split Array Largest Sum, Minimize Maximum Distance to Gas Station, Magnetic Force Between Balls (maximize minimum variant).
The predicate pattern: "Given parameter = mid, simulate the process greedily. Does it satisfy the constraint?"
Setting right too small. For "minimum capacity," the upper bound must be the sum of all elements (worst case: one trip), not the max element. If right is too small, the answer might be outside the search space and you'll get a wrong answer with no error.
Binary Search on Answer — Maximize the Minimum
Representative problems: Aggressive Cows, Magnetic Force Between Balls, Allocate Minimum Number of Pages.
The predicate pattern: "Given minimum value = mid, greedily check if the constraint can be satisfied."
Forgetting to round UP for mid. This family uses left = mid (keep mid as candidate), which requires mid = left + (right - left + 1) / 2. With standard rounding (down), you get an infinite loop when right = left + 1.
Rotated Sorted Arrays
Key insight: In a rotated sorted array, one half of any split is always sorted. By comparing arr[mid] with arr[right], you determine which half contains the pivot.
Representative problems: Find Minimum in Rotated Sorted Array (I & II), Search in Rotated Sorted Array (I & II).
When the array has duplicates (Rotated Sorted Array II), arr[mid] == arr[right] can happen and you can't determine which half is sorted. Handle this by decrementing right-- (skip the duplicate) — this degrades to O(n) in the worst case but is correct.
Peak / Valley Finding
Key insight: You don't need a sorted array. The slope (ascending/descending) tells you which direction to go. A peak is guaranteed to exist because the boundaries are "virtual -∞."
Representative problems: Find Peak Element, Find Peak Element II (2D), Mountain Array (find the peak then search both sides).
Binary Search + Greedy
Representative problems: Koko Eating Bananas, Ship Packages, Aggressive Cows, Allocate Pages, Painter's Partition.
Binary Search + Prefix Sum
Representative problems: Minimum Size Subarray Sum, Range Sum queries with optimization.
Matrix Binary Search
Representative problems: Search a 2D Matrix (I & II), Kth Smallest Element in a Sorted Matrix.
Binary Search on Real Numbers
Representative problems: Sqrt(x), Find the Median of Two Sorted Arrays (partial), Minimize Max Distance to Gas Station (real-valued variant).
Using too few iterations or too large an epsilon. If the problem requires 6 decimal places, use epsilon = 1e-8 or run 100 iterations. Also: forgetting that left = mid (not mid + 1) for reals — there are infinitely many values between mid and mid+1.
Infinite / Unknown-Size Arrays
Key insight: You don't need to know n. Exponential growth finds a bound in O(log position) steps. The binary search within that bound is also O(log position). This is why it's called "exponential search" — the bound grows exponentially, but the total cost is logarithmic.
Representative problems: Search in Sorted Array of Unknown Size, Search in Infinite Sorted Array.
Not handling the "out of bounds" sentinel. When reader.get(hi) returns a sentinel like Integer.MAX_VALUE, you must stop doubling and set hi to the last valid index — otherwise you binary search into invalid territory.
Binary Search + Graph
Key insight: The predicate is a graph traversal: "can I reach the destination using only edges with weight ≤ mid?" If yes, the minimum max-weight is ≤ mid. Monotonic: more edges allowed = easier to reach. This turns a graph problem into BS + BFS/DFS.
Representative problems: Path With Minimum Effort, Find the Safest Path in a Grid, Swim in Rising Water, Reachable Nodes in Subdivided Graph.
The predicate pattern: Build a subgraph with edges ≤ mid. Run BFS/DFS from source to destination. If reachable, check(mid) = true.
Forgetting that the graph traversal in the predicate runs in O(V+E) each time, making the total O((V+E) × log(maxWeight)). Make sure constraints allow this. Also: ensure the monotonicity — "reachable with max weight ≤ mid" must be monotonic (if reachable with max weight X, also reachable with max weight X+1).
Binary Search + DP
Key insight: Sometimes the feasibility check isn't a simple greedy — it's a DP. The outer binary search narrows the answer, and the inner DP verifies whether a given threshold is achievable. The total complexity is O(DP_time × log(range)).
Representative problems: Split Array Largest Sum (can also be greedy), Kth Smallest Subarray Sum, Capacity to Ship (when the greedy doesn't work and you need DP for the check), Median of Two Sorted Arrays (BS + partition logic that resembles DP).
The predicate pattern: check(mid) runs a DP that computes the minimum/maximum achievable cost. If that cost ≤ mid, return true. The DP exploits optimal substructure within the feasibility check.
Using DP in the predicate when a greedy would suffice. Always try the greedy predicate first — it's O(n) instead of O(n²) or O(n×k). Only switch to DP when the greedy fails (when locally-optimal choices don't lead to global optimality in the predicate).
Debugging, Interview Thinking & Problem Roadmap
The practical toolkit: how to fix a broken binary search, how to think out loud in an interview, and which problems to practice in what order.
13 · Debugging Binary Search
When your binary search is wrong, run through this checklist in order:
Is the invariant stated and correct?
Write it down: "At the start of each iteration, ___." If you can't state it, you don't have one. Go back to Part III.
Is the search space correct?
Are left and right initialized to the absolute min and max of the answer? Common error: right = n-1 when the answer could be n.
Is the predicate monotonic?
Test it manually: is there any case where check(x) is true but check(x+1) is false (for a "find first true" problem)? If yes, your predicate is wrong.
Do the boundary moves match the invariant?
When check(mid) is true, does the boundary update correctly narrow the search space while preserving the invariant? When false, same question.
Is mid rounding correct?
If you set left = mid, are you rounding UP? If you set right = mid, are you rounding DOWN? Mismatched rounding → infinite loop.
Is the return value correct?
If loop is left < right, return left (or right, they're equal). If loop is left <= right, return the stored answer variable.
Edge cases?
Test: array of size 1, target smaller than all elements, target larger than all, all elements the same, answer at the boundary.
14 · Interview Thinking — the monologue
In an interview, you need to think out loud — including the wrong turns. Here's a worked interview monologue on an unseen problem, showing every decision including the ones that didn't work.
Problem: "Given an array of positions and m balls, maximize the minimum distance between any two balls."
Me: Okay, so we're placing m balls in stalls and we want to maximize the minimum distance between any pair. Let me think about this...
Wrong first idea: Maybe I can use a greedy — sort the positions and place balls evenly? Place the first and last, then divide the gap into m-1 equal parts?
Why that's wrong: No — the positions are discrete. If I have stalls at [1, 2, 8, 14, 16] and 3 balls, "even spacing" would be at 1, 8.5, 16 — but 8.5 isn't a stall. Greedy doesn't directly give the optimal placement.
Second idea: What if I try every possible minimum distance d, from 0 to max-min, and check if I can place m balls with every pair ≥ d apart? That's a linear scan over d, and for each d I greedily verify in O(n). Total: O((max-min) × n). That works but it's slow if max-min is 10⁹.
The key question: Is the feasibility monotonic? If I can place balls with minimum distance d, can I also place them with distance d-1? Yes — less restrictive. So the answers are YYYYYNNNNN as d increases.
The binary search insight: I can binary search on d! The search space is [0, max(positions)-min(positions)]. My predicate check(d) greedily places balls: put the first at position[0], then each next at the first stall ≥ last + d. If I can place all m, return true.
Template: I'm finding the last Y (maximum feasible d), so this is Template 6. I'll use left = mid when check is true (try bigger), right = mid - 1 when false (try smaller). And I need to round mid UP to avoid the infinite loop.
Let me verify the invariant: "The maximum feasible d is in [left, right]." When check(mid) is true, mid is feasible, so the max is ≥ mid — I keep mid. When false, mid is too big — I exclude it. The loop ends when left == right, and that's the answer.
Time: O(n log(max-min)) — n for the greedy check, log(range) iterations. That's efficient enough for n ≤ 10⁵ and max-min ≤ 10⁹."
Notice the structure: wrong idea → why it fails → second idea → is it monotonic? → yes → binary search → search space → predicate → template → invariant → time complexity. The wrong idea isn't a waste — it builds the brute-force understanding that motivates the binary search. Interviewers want to see this reasoning, not just the final answer.
15 · Problem Roadmap
These problems are ordered to build intuition progressively. Each stage introduces one new idea. Don't skip stages.
Stage 1: Foundations — solidify the basics
- Binary Search EasyTemplate 1. Nail the standard implementation.
- Find First and Last Position EasyTemplates 2+3+4. Lower bound and upper bound.
- Search Insert Position EasyLower bound variant. What if target isn't found?
Stage 2: Rotated arrays — learn to exploit partial order
- Find Minimum in Rotated Sorted Array MediumTemplate 7. The pivot is the transition point.
- Search in Rotated Sorted Array MediumFind the pivot, then search the correct half.
Stage 3: Peaks — binary search without sorted arrays
- Find Peak Element MediumTemplate 8. Slope-based binary search.
Stage 4: BS on Answer — the big leap
- Koko Eating Bananas MediumThe canonical BS-on-answer problem. Template 5.
- Capacity to Ship Packages MediumSame pattern, different predicate. Build confidence.
- Split Array Largest Sum HardBS on answer + greedy predicate. The predicate is trickier.
- Minimize Max Distance to Gas Station HardBS on real numbers. Continuous search space.
Stage 5: Maximize the Minimum — the mirror image
- Magnetic Force Between Two Balls MediumTemplate 6. Maximize the minimum distance.
- Aggressive Cows MediumSame pattern, different story. (SPOJ)
Stage 6: Hidden binary search — non-obvious applications
- Find the Duplicate Number MediumBS on values using pigeonhole counting. Not on indices!
- Kth Smallest in Sorted Matrix MediumBS on value + count elements ≤ mid in the matrix.
- Median of Two Sorted Arrays HardBS on partition point. The hardest classic.
Stage 7: Mountain, Matrix & Infinite — structural variants
- Search in Rotated Sorted Array II MediumDuplicates in the rotated array. The ≤ vs < matters.
- Find in Mountain Array HardTemplate 9. Find the peak, then search both slopes.
- Search a 2D Matrix MediumTemplate 11A. Flatten the matrix.
- Search a 2D Matrix II MediumTemplate 11B. Staircase search from top-right.
Stage 8: BS + Graph & DP — combining techniques
- Path With Minimum Effort MediumBS on effort + BFS/DFS in the predicate. Family 10.
- Swim in Rising Water HardBS on water level + BFS. Same BS+Graph pattern.
- Minimize Max Distance to Gas Station HardBS on real numbers (continuous). Family 8.
16 · Cheat Sheets
Binary Search Recognition Checklist
Template Selection Flowchart
| Problem asks for… | Template | Key decision |
|---|---|---|
| Exact value in sorted array | 1 (left ≤ right) | Return mid when found |
| First element ≥ target | 2 (lower bound) | right = mid when ≥ |
| First element > target | 3 (upper bound) | right = mid when > |
| Minimum feasible value | 5 (first true) | right = mid when check true |
| Maximum feasible value | 6 (last true) | left = mid when check true, ROUND UP |
| Minimum in rotated array | 7 (pivot) | Compare arr[mid] with arr[right] |
| Peak element | 8 (slope) | Compare arr[mid] with arr[mid+1] |
Predicate Design Checklist
Boundary Movement Guide
| When check(mid) is TRUE | When check(mid) is FALSE |
|---|---|
| Find first true: right = mid (mid could be the answer) | Find first true: left = mid + 1 (mid is not the answer) |
| Find last true: left = mid (mid could be the answer) | Find last true: right = mid - 1 (mid is not the answer) |
| Exact search: return mid! | Exact search: shrink toward the target |
Common Bugs Checklist
Answer-Storage Guide
| Loop condition | Store answer? | Why | What to return |
|---|---|---|---|
left < right | No — just return left | Loop ends when left == right. Invariant collapses to "answer IS left." No variable needed. | return left (or right, they're equal) |
left <= right | Yes — store ans = mid when valid | Loop ends when left > right — search space is empty. Neither boundary is the answer. Must save the best candidate. | return ans |
right - left > ε (continuous) | No — return left or (left+right)/2 | Loop ends when the range is tiny. left and right are within ε of the true answer. | return left or return (left+right)/2 |
Return-Value Guide
| You're finding… | Loop | Return | Why |
|---|---|---|---|
| First element ≥ target (lower bound) | left < right | left | left == right == first valid index |
| Last element ≤ target | left < right | left - 1 or right | Depends on predicate — verify with your invariant |
| Minimum feasible value | left < right | left | left == right == first value where check is true |
| Maximum feasible value | left < right | left | left == right == last value where check is true |
| Exact target (found or not) | left ≤ right | mid (found) or -1 (not) | Return immediately when found; -1 after loop |
| Minimum in rotated array | left < right | arr[left] | left == right == index of minimum |
| Peak element | left < right | left | left == right == index of a peak |