Notes
DSA/ Binary Search Masterclass
DSA · The Thinking Course

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.

For: Java solvers who can code basic BS but freeze on modified problems Style: first-principles · interactive Goal: solve unseen problems
By the end, binary search will stop feeling like a bag of templates you either recognize or don't. You'll own a repeatable thinking process for determining what to search over, what property is monotonic, what the predicate should be, and why each boundary moves. That process — not memorized code — is the whole game.
i

How to actually use this page

This is long on purpose — it's a course, not a cheat sheet. Read it like this:

01

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.

02

Guess before revealing

Every purple "pause & think" box is a real question. Commit to an answer. Reveals only help once you've tried.

03

Live in Parts III–V

The invariant framework and the predicate design process are the reusable skill. Everything else is practice for them.

ii

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.


Part I

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:

Binary search is maintaining a correct search invariant while shrinking an ordered search space using a monotonic property. The "ordered search space" can be indices, values, or abstract answers. The "monotonic property" is a yes/no question whose answer transitions from all-No to all-Yes (or vice versa) exactly once. The "invariant" is the promise your 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

Question 1
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?

Question 2
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.

Question 3
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."

Analogy · the dictionary game

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.

Monotonic = searchable
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.

Non-monotonic = not searchable
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.

Reframe

"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.

Quick check
What is the genuinely hard, transferable skill in binary search problems?

Part II · recognition

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:

Q1 · Objective

Does the problem ask for a specific position (find, first, last), an extremum (minimum, maximum, smallest, largest), or a threshold (earliest, latest, feasible)?

▼ yes — binary search is on the table; keep going
Q2 · The monotonicity test

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?

Yes → binary searchYou have a monotonic predicate. Binary search finds the transition point in O(log n). Now design the search space and predicate (Parts III–IV).
No → binary search is dead hereIf the predicate oscillates (FTFTFT…), there's no "half to discard." You need a different approach — DP, graph, greedy, or brute force.

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?"

Extremum signals
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.

Boundary signals
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.

Feasibility signals
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.

Optimization signals
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 strongest positive signals for binary search

① 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.

When binary search does NOT apply

① 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 forTell / why not binary search
asks for min/max of a monotonic feasibility functionBinary search on answerThe feasibility check creates the FFFFTTTT pattern.
has a sorted array and asks for a positionStandard binary searchThe sorted order IS the monotonic property.
asks for all pairs / all subsets / count of somethingTwo pointers / Hashing / DPBinary search finds one transition point, not all solutions.
optimizes over choices with overlapping subproblemsDynamic programmingNo single monotonic parameter to bisect — you need to compare futures.
processes elements in sorted order with a local commitmentGreedyThe sort enables greedy, not binary search. (Though greedy inside BS on answer is common.)
asks shortest path / connectivity / traversalBFS / Dijkstra / DFSThe 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.


Part III · the core machinery

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:

Bug · returning the wrong variableYou return 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.
Fix · state the invariant firstIf your invariant is "the answer is in [left, right]," then when 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.
Bug · infinite loopleft = 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."
Fix · ensure progressEither use 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.

Binary Search Visualizer
Target:
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

Type A
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.

Type B
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.

Type C
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.

Common mistake · wrong search space

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

Step 1
Identify the parameter

What is mid representing? An index? A capacity? A distance? A speed?

Step 2
State the question

"Given that mid is the answer, can I verify it in O(n) or O(n log n)?"

Step 3
Write the check

Implement the verification. This is often a greedy simulation, a counting argument, or a simple comparison.

Step 4
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:

Incorrect predicates — and why they fail

Seeing a wrong predicate is more instructive than seeing a right one. Here are two common mistakes:

Wrong predicate · Split Array Largest Sum

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.

Correct predicate

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 predicate · Koko Eating Bananas

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.

Correct predicate

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.

The "≤ vs ==" lesson

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.

Predicate Simulator
Scenario:

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:

If check(mid) is TRUE
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).

If check(mid) is FALSE
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.

Boundary Movement Simulator

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.

Store answer during loop
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.

Don't store — return a boundary
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 == right
The #1 answer-storage mistake

Mixing 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).


Part IV · templates

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)

Template · search

Find exact value in sorted array

Invariant: answer is in [left, right] · Loop: left <= right · Return: index or -1
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)

Template · boundary

Find first position where arr[i] ≥ target

Invariant: answer is in [left, right] · Loop: left < right · Return: left
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)

Template · boundary

Find first position where arr[i] > target

Same structure as lower bound, predicate changes to arr[mid] > 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

Template · position

Find first/last index of target in sorted array with duplicates

Combines lower bound + upper bound
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)

Template · optimization

Find the minimum value of x such that check(x) is true

Pattern: NNNNNYYYYY · Find first Y · Loop: left < right
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)

Template · optimization

Find the maximum value of x such that check(x) is true

Pattern: YYYYYNNNNN · Find last Y · Loop: left < right · mid rounds UP
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.

The infinite loop trap

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)

Template · rotated

Find the minimum in a rotated sorted array

The minimum is the pivot point where the rotation happened
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

Template · peak

Find any peak element (greater than both neighbors)

The slope tells you which direction the peak is in
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.

Common mistake

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)

Template · mountain

Search in a bitonic (mountain) array

Invariant: peak is in [left, right] · Then: two separate BS on each slope

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;
}
Common mistake

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)

Template · infinite

Search in a sorted array of unknown length

First find a bound, then binary search within it

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.

Common mistake

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)

Template · matrix

Search in a 2D sorted matrix

Variant A: fully sorted (flatten) · Variant B: row/col sorted (staircase)

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.

Common mistake

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)

Template · continuous

Binary search over real numbers

Loop: while (right - left > epsilon) · Return: left or (left+right)/2
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.

Common mistake

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:

Step 1

Is the array sorted (or rotated sorted)?

Yes → search on indicesUse Template 1 (exact match), 2/3 (lower/upper bound), 4 (first/last occurrence), 7 (rotated min), or 8 (peak). The search space is array indices.
No → continueThe array isn't sorted. Is there a range of possible answers you can bisect?
Step 2

Does the problem ask for a minimum or maximum of some value, where feasibility is monotonic?

Yes → binary search on answerUse Template 5 (find min feasible) or 6 (find max feasible). The search space is the range of possible answers.
No → binary search might not applyRe-examine the problem. Is there a hidden monotonic property? If not, try DP, greedy, or graph algorithms.

The master comparison table

TemplateSearch SpaceLoopMid RoundingWhen check(mid) trueWhen check(mid) falseReturn
1 · Exact search[0, n-1]left ≤ rightdownreturn midshrink both sides-1 if not found
2 · Lower bound[0, n]left < rightdownright = midleft = mid + 1left
3 · Upper bound[0, n]left < rightdownright = midleft = mid + 1left
4 · First/Last occ.[0, n]left < rightdownright = midleft = mid + 1left / upper-1
5 · Min feasible[lo, hi]left < rightdownright = midleft = mid + 1left
6 · Max feasible[lo, hi]left < rightUPleft = midright = mid - 1left
7 · Rotated min[0, n-1]left < rightdownright = midleft = mid + 1arr[left]
8 · Peak[0, n-1]left < rightdownright = midleft = mid + 1left
9 · Mountaintwo phasesleft ≤ rightdownreturn midshrinkindex or -1
10 · Infinite[hi/2, hi]left ≤ rightdownreturn midshrinkindex or -1
11 · Matrix A[0, rows×cols-1]left ≤ rightdownreturn trueshrinkboolean
11 · Matrix Bstaircaserow/col walkn/areturn truemove left/downboolean
12 · Continuous[lo, hi] realhi-lo > εdownright = midleft = midleft
The rounding rule, simplified

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.


Part V · the thought process

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.

1
Read & classify

What is the problem asking? Min/max? First/last? Exact position? Does it smell like BS?

2
Brute force

How would I solve this without BS? Linear scan? Try all values? This reveals the structure.

3
Find monotonicity

What property transitions from false→true (or true→false) as some parameter increases?

4
Define search space

What is the parameter? What are its absolute min and max? That's [left, right].

5
Write predicate

"Given parameter = mid, can I verify feasibility in O(n)?" Write check(mid).

6
Choose template

Min feasible? Max feasible? First true? Select the template and derive boundary moves from the invariant.

7
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;
}
Common mistake

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.


Part VI · pattern library

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

Family 1

Binary Search on Answer — Minimize the Maximum

"Find the minimum value of X such that [some constraint is satisfied]"
→ Problem says "minimize the maximum" or "minimum capacity/speed/threshold"
→ Feasibility is monotonic: if X works, X+1 also works
→ The check function is often a greedy simulation
→ Template 5: find first true in NNNNNYYYYY

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?"

Common mistake

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.

Family 2

Binary Search on Answer — Maximize the Minimum

"Find the maximum value of X such that [some constraint is satisfied]"
→ Problem says "maximize the minimum" or "largest minimum distance"
→ Feasibility is monotonic: if X works, X-1 also works
→ Template 6: find last true in YYYYYNNNNN · Round UP

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."

Common mistake

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.

Family 3

Rotated Sorted Arrays

A sorted array was rotated at some pivot. Find the pivot, search for a target, etc.
→ "Rotated sorted array" in the problem statement
→ Compare arr[mid] with arr[right] (or arr[left]) to determine which half is sorted
→ The pivot/minimum is the transition point

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).

Common mistake

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.

Family 4

Peak / Valley Finding

Find a local extremum using slope information
→ "Peak element" — greater than both neighbors
→ Compare arr[mid] with arr[mid+1] to determine slope
→ Ascending → peak is to the right. Descending → peak is at mid or left.

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).

Family 5

Binary Search + Greedy

The feasibility check inside BS on answer is a greedy algorithm
→ BS on answer + "can we distribute/partition/assign?"
→ The check function greedily assigns elements to groups/bins
→ Often involves sorting first, then a linear sweep in the predicate

Representative problems: Koko Eating Bananas, Ship Packages, Aggressive Cows, Allocate Pages, Painter's Partition.

Family 6

Binary Search + Prefix Sum

Use prefix sums to make the feasibility check O(1) or O(log n)
→ Subarray sum queries + optimization
→ Prefix sum array is sorted (if all elements positive) → binary search on it
→ "Shortest subarray with sum ≥ k" → prefix sums + monotonic deque or BS

Representative problems: Minimum Size Subarray Sum, Range Sum queries with optimization.

Family 7

Matrix Binary Search

Search in a 2D structure with sorted rows/columns
→ "Search a 2D matrix" where rows are sorted and row[i][0] > row[i-1][last]
→ Flatten to 1D: index = row * cols + col
→ Or: search in a matrix where rows AND columns are sorted (staircase search)

Representative problems: Search a 2D Matrix (I & II), Kth Smallest Element in a Sorted Matrix.

Family 8

Binary Search on Real Numbers

Continuous search space, terminate when precision is sufficient
→ "Find the square root" or "find the optimal real-valued parameter"
→ Loop: while (right - left > epsilon)
→ epsilon = 1e-7 or 1e-9 typically

Representative problems: Sqrt(x), Find the Median of Two Sorted Arrays (partial), Minimize Max Distance to Gas Station (real-valued variant).

Common mistake

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.

Family 9

Infinite / Unknown-Size Arrays

Exponential search — find the bound, then binary search
→ Array has no known size (stream, reader interface, "infinite")
→ Double the index until you find an element ≥ target (or out of bounds)
→ Then binary search within [bound/2, bound]

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.

Common mistake

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.

Family 10

Binary Search + Graph

Binary search on the answer, with a graph traversal in the predicate
→ "Can you reach node B from node A with cost ≤ X?" → BS on X, BFS/DFS in the predicate
→ "Minimum maximum edge weight on a path" → BS on max weight, check connectivity with edges ≤ mid
→ "Kth smallest path cost" → BS on the cost, count paths ≤ mid

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.

Common mistake

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).

Family 11

Binary Search + DP

Binary search narrows the search space, DP verifies the predicate
→ "Split into k groups with minimum total cost" where the split has optimal substructure
→ The predicate "can we achieve cost ≤ mid?" is itself an optimization solved by DP
→ "Kth smallest sum of a subsequence" → BS on the sum, count subsequences ≤ mid with 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.

Common mistake

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).


Part VII · reference & practice

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⁹."

The pattern of the monologue

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

  1. Binary Search EasyTemplate 1. Nail the standard implementation.
  2. Find First and Last Position EasyTemplates 2+3+4. Lower bound and upper bound.
  3. Search Insert Position EasyLower bound variant. What if target isn't found?

Stage 2: Rotated arrays — learn to exploit partial order

  1. Find Minimum in Rotated Sorted Array MediumTemplate 7. The pivot is the transition point.
  2. Search in Rotated Sorted Array MediumFind the pivot, then search the correct half.

Stage 3: Peaks — binary search without sorted arrays

  1. Find Peak Element MediumTemplate 8. Slope-based binary search.

Stage 4: BS on Answer — the big leap

  1. Koko Eating Bananas MediumThe canonical BS-on-answer problem. Template 5.
  2. Capacity to Ship Packages MediumSame pattern, different predicate. Build confidence.
  3. Split Array Largest Sum HardBS on answer + greedy predicate. The predicate is trickier.
  4. Minimize Max Distance to Gas Station HardBS on real numbers. Continuous search space.

Stage 5: Maximize the Minimum — the mirror image

  1. Magnetic Force Between Two Balls MediumTemplate 6. Maximize the minimum distance.
  2. Aggressive Cows MediumSame pattern, different story. (SPOJ)

Stage 6: Hidden binary search — non-obvious applications

  1. Find the Duplicate Number MediumBS on values using pigeonhole counting. Not on indices!
  2. Kth Smallest in Sorted Matrix MediumBS on value + count elements ≤ mid in the matrix.
  3. Median of Two Sorted Arrays HardBS on partition point. The hardest classic.

Stage 7: Mountain, Matrix & Infinite — structural variants

  1. Search in Rotated Sorted Array II MediumDuplicates in the rotated array. The ≤ vs < matters.
  2. Find in Mountain Array HardTemplate 9. Find the peak, then search both slopes.
  3. Search a 2D Matrix MediumTemplate 11A. Flatten the matrix.
  4. Search a 2D Matrix II MediumTemplate 11B. Staircase search from top-right.

Stage 8: BS + Graph & DP — combining techniques

  1. Path With Minimum Effort MediumBS on effort + BFS/DFS in the predicate. Family 10.
  2. Swim in Rising Water HardBS on water level + BFS. Same BS+Graph pattern.
  3. Minimize Max Distance to Gas Station HardBS on real numbers (continuous). Family 8.

16 · Cheat Sheets

Binary Search Recognition Checklist

min / max smallest / largest first / last earliest / latest is it possible? feasible? minimize the maximum maximize the minimum threshold sorted array rotated array peak element

Template Selection Flowchart

Problem asks for…TemplateKey decision
Exact value in sorted array1 (left ≤ right)Return mid when found
First element ≥ target2 (lower bound)right = mid when ≥
First element > target3 (upper bound)right = mid when >
Minimum feasible value5 (first true)right = mid when check true
Maximum feasible value6 (last true)left = mid when check true, ROUND UP
Minimum in rotated array7 (pivot)Compare arr[mid] with arr[right]
Peak element8 (slope)Compare arr[mid] with arr[mid+1]

Predicate Design Checklist

What does mid represent? Can I verify mid in O(n)? Is check(x) monotonic? Test: check(lo) and check(hi) Test: check(x) true → check(x+1) true?

Boundary Movement Guide

When check(mid) is TRUEWhen 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

Infinite loop → check rounding Wrong return → check invariant Off-by-one → check ±1 on boundary Overflow → use left + (right-left)/2 Missing edge case → test n=1, all same Wrong template → re-check monotonicity direction

Answer-Storage Guide

Loop conditionStore answer?WhyWhat to return
left < rightNo — just return leftLoop ends when left == right. Invariant collapses to "answer IS left." No variable needed.return left (or right, they're equal)
left <= rightYes — store ans = mid when validLoop 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)/2Loop 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…LoopReturnWhy
First element ≥ target (lower bound)left < rightleftleft == right == first valid index
Last element ≤ targetleft < rightleft - 1 or rightDepends on predicate — verify with your invariant
Minimum feasible valueleft < rightleftleft == right == first value where check is true
Maximum feasible valueleft < rightleftleft == right == last value where check is true
Exact target (found or not)left ≤ rightmid (found) or -1 (not)Return immediately when found; -1 after loop
Minimum in rotated arrayleft < rightarr[left]left == right == index of minimum
Peak elementleft < rightleftleft == right == index of a peak

Off-by-One Checklist

Did I exclude mid when it's NOT the answer? → use ±1 Did I keep mid when it IS a candidate? → use = mid Is right = n or n-1? Check if answer can be "past the end" Am I rounding UP when using left = mid? Am I rounding DOWN when using right = mid? Does the search space shrink every iteration? Test: answer at index 0, at index n-1, not in array Test: all elements same, single element, two elements