Interview Study Notebook

Binary Search,
derived not memorized.

You understand binary search after you see the solution. This notebook fixes the other direction: looking at an unfamiliar problem and reconstructing the search from first principles.

6 reasoning patterns 23 curated problems 6 iteration-level walkthroughs 1 question you always ask first
Before any pattern

The one question, and the derivation drill

Binary search is not "a sorted array + two pointers." It is one idea wearing many costumes:

The invariant behind every binary search

There is a point where the answer flips from NO to YES, and testing the middle tells me which side that flip is on. Every half I throw away, I throw away because I have proven the flip cannot be there.

So the real question is never "how do I write the loop." It is:

Ask this first, every time

"What fact lets me confidently eliminate half the remaining possibilities?"

If you can name that fact, the code writes itself. If you cannot, no template will save you. The eight questions below turn that fact into working code — run them in order on any candidate problem.

The 8-question derivation drill

  1. What is my search space? An array of indices? A range of possible answers [lo, hi]? A value domain? Name it explicitly.
  2. What does mid mean? Say it in words: "mid is a candidate eating speed," "mid is an index," "mid is a possible k-th value." If you can't finish the sentence, you're not ready to code.
  3. What property is monotonic? As the candidate increases, does some true/false condition change at most once (F…F T…T or T…T F…F)? If nothing is monotonic, binary search does not apply.
  4. Do I want the first valid or the last valid point? Minimum feasible speed? First index ≥ target? Last position that still works? This single choice decides your whole template.
  5. If mid works, which half is safe to discard? Prove it from the monotonic property, not from intuition.
  6. If mid fails, which half is safe to discard? Same — prove it.
  7. When does the loop stop? lo < hi converges the two pointers onto the boundary; lo <= hi is for "found it, return now" exact-match searches.
  8. What do I return? lo (the boundary), a stored candidate ans, or an index. Know this before the loop, not after.
Why you get stuck on new problems

You reach for the code (question 7 and 8) before answering questions 1–4. The costume changes every problem; the four questions underneath never do. Train the questions, not the loops.

A note on how I organized this

Your draft mixed "find peak," "rotated," and "classic" into one bucket, and split lower/upper bound off on their own. I regrouped around reasoning technique, not surface features. Boundary search (lower/upper bound) is not its own pattern — it is the engine, so it opens the notebook and every later pattern reuses it. Peak and rotated searches are separated from classic search because their monotonic property is a local shape, not sorted order — a genuinely different mental move. The result is six patterns, each teaching something the others don't.

The engine

The unifying idea: find the first TRUE

Almost every binary search you will write in an interview is secretly the same search: find the leftmost position where a monotonic predicate becomes true. Write it once, understand it deeply, and reuse it everywhere.

Picture the search space laid flat with the predicate evaluated at each spot. Monotonic means it looks like this — never like F T F T:

0F
1F
2F
3T
4T
5T
6T
predicate false predicate true ↑ we want index 3 — the first TRUE (the boundary)

The whole game: keep a window [lo, hi) that is guaranteed to contain the boundary. Test the middle. Because the strip is monotonic, one answer at mid proves an entire half is safe to drop.

If check(mid) is TRUE

The boundary is at mid or to its left. Everything right of mid is also true and can't be the first true. Keep mid: hi = mid.

If check(mid) is FALSE

The boundary is strictly to the right. mid itself is false, so it can't be the answer. Discard it: lo = mid + 1.

Step through it yourself

Predicate strip below. Watch how lo/hi squeeze onto the first TRUE, and how each step provably deletes a half. Reshuffle to see the boundary move.

The two templates you actually need

Ninety percent of interview binary searches are one of these two. Learn why the boundaries differ and you never guess again.

A · First TRUE (lower bound) — the workhorse

int lo = 0, hi = n;        // [lo, hi) — hi is ONE PAST the end
while (lo < hi) {         // stop when window is empty
    int mid = lo + (hi - lo) / 2;
    if (check(mid)) hi = mid;   // mid may be the answer, keep it
    else            lo = mid + 1; // mid fails, drop it
}
return lo;               // first true index (== n if none)

B · Exact match — only when you want equality

int lo = 0, hi = n - 1;    // [lo, hi] — both ends inclusive
while (lo <= hi) {        // stop when they cross
    int mid = lo + (hi - lo) / 2;
    if (a[mid] == target) return mid;
    else if (a[mid] < target) lo = mid + 1;
    else                    hi = mid - 1;
}
return -1;              // not present
Why the boundaries differ (read this twice)

Template A converges onto a boundary that always exists, so it never needs to "return from inside the loop" — it narrows [lo,hi) until lo == hi lands exactly on the first true. It uses hi = mid (not mid-1) precisely because mid could be the answer and must survive.

Template B is hunting for a specific value that may not exist, so it can quit early with return mid, and safely does mid±1 everywhere because a matched mid is returned, not discarded.

Default to A. Boundary search subsumes "first occurrence," "last occurrence," "insertion point," "first ≥ target," "first > target," and every binary-search-on-answer problem. You reshape check; the skeleton never moves.

Getting last/upper variants from the same skeleton

You want… Define check(i) as Answer is
First index with a[i] ≥ target (lower bound) a[i] >= target lo
First index with a[i] > target (upper bound) a[i] > target lo
Last index with a[i] < target lower bound of target, then lo - 1
Last index with a[i] <= target upper bound of target, then lo - 1
Count of elements < target lower bound of target lo
Memory hook

Don't memorize four functions named lower_bound / upper_bound. Memorize one move: "find first TRUE, then adjust by ±1 or read the count." Everything to the left of lo is false; everything from lo on is true. Last-anything = lo - 1.

Pattern 1

Classic search & boundaries

A · Pattern intuition

The costume you already know: a sorted array, and you want a value, a position, or a boundary inside it. The monotonic property is handed to you for free — sorted means "everything left of the answer is smaller, everything right is bigger." You binary search over indices; mid is an index; you discard the half that provably can't contain what you're looking for.

The trap here is thinking this pattern is only "find target." Ninety percent of interview value is the boundary flavor: first occurrence, last occurrence, insertion point, first element ≥ x. All of those are Template A with a one-line predicate. Reach for exact-match (Template B) only when the literal answer is "does this value exist, and where."

What triggers this pattern

The word "sorted" appears (or you can cheaply sort), and you want a single position/value. If the array is sorted but you're optimizing a quantity, you're probably in Pattern 3, not here.

B · Visual — one comparison halves the world

02
15
28
3·mid12
416
523
638
target = 16. a[mid]=12 < 16, so 16 (if present) is strictly right → indices 0–3 vanish in one step.

C · Template

For exact match use Template B from the section above. For every boundary flavor, use Template A. Below: insertion position, which is just "first index whose value is ≥ target."

// Search Insert Position == lower bound of target
int lo = 0, hi = nums.length;      // [lo, hi)
while (lo < hi) {
    int mid = lo + (hi - lo) / 2;
    if (nums[mid] >= target) hi = mid; // candidate, keep
    else                    lo = mid + 1;
}
return lo;   // where target belongs (0..n)

D · How to derive it here

Run the drill: (1) search space = indices [0, n]. (2) mid = an index. (3) monotonic property = "nums[i] ≥ target" flips false→true exactly once because sorted. (4) I want the first true → Template A. (5) mid true → answer is mid-or-left, hi=mid. (6) mid false → answer is strictly right, lo=mid+1. (7) stop at lo==hi. (8) return lo. Notice you never touched a single line of code from memory.

Deep dive · iteration by iteration

704. Binary Search — find target = 9

Array [1, 3, 5, 7, 9, 11], exact-match (Template B), lo=0 hi=5.

1
lo=0, hi=5 → mid=2. a[2]=5 < 9. Target is right of mid. Discard 0–2 → lo=3.
2
lo=3, hi=5 → mid=4. a[4]=9 == 9. Match. Return 4.
01
13
25
37
49
511

Two comparisons for six elements. With a million elements it would be ~20. That's the whole reason this exists.

Curated problems — foundation → variation

Foundation

Why it matters: The pure skeleton. Write it until closed-interval exact match is muscle memory, then never write it from scratch again — everything else is a predicate swap.

"Sorted array, return the index of target or -1."
Before you solve
  • Inclusive [lo,hi] or half-open [lo,hi)? Pick one and stay consistent.
  • Do I return from inside the loop, or after?

Why it matters: Your first taste that "not found" is not failure — it's a position. This is lower bound in disguise and the gateway to boundary thinking.

"If it's not there, where would it go to keep the array sorted?"
Before you solve
  • What should the loop return when target is larger than everything?
Hint ladder
Hint 1 · DirectionDon't search for equality. Search for the first slot that is "big enough."
Hint 2 · Search spacelo=0, hi=n (yes, n, not n−1 — the answer can be "past the end").
Hint 3 · Key observationPredicate is nums[mid] >= target. Return lo. This is literally lower bound.
Recognition

Why it matters: The predicate is given to you as an API call, not an array comparison. Forces you to see binary search as "find first TRUE" with zero sorted array in sight.

"Versions go good…good, bad…bad. Find the first bad one."
Before you solve
  • What is the F…F T…T strip here? (Answer: isBadVersion(i).)
  • Watch for integer overflow computing mid.
Hint ladder
Hint 1 · DirectionThis is Template A verbatim. check(i) = isBadVersion(i).
Hint 2 · Search spacelo=1, hi=n. mid = a version number.
Hint 3 · Key observationUse mid = lo + (hi-lo)/2, never (lo+hi)/2 — n can be ~2³¹.

Why it matters: Proves one search isn't special — you run boundary search twice (lower bound of target, upper bound of target) and read the range between. The clearest "duplicates need boundaries" lesson.

"Sorted array with repeats — give me the span [first, last] of target."
Before you solve
  • What two predicates give me the left edge and the right edge?
  • How do I detect target is absent from the two boundaries alone?
Hint ladder
Hint 1 · DirectionTwo independent boundary searches, not one clever pass.
Hint 2 · Search spaceLeft = first index with a[i] >= target. Right = (first index with a[i] > target) − 1.
Hint 3 · Key observationIf left > right or a[left] != target, target is absent → return [-1,-1].
Variation

Why it matters: Strict upper bound with a wrap-around twist. Tiny change to the predicate (> not >=) and a modulo at the end — cements that the predicate, not the skeleton, carries the meaning.

"Smallest letter strictly greater than target; letters wrap around."
Hint ladder
Hint 1 · DirectionUpper bound: first index with letters[i] > target.
Hint 2 · Search spacelo=0, hi=n. Answer index might be n.
Hint 3 · Key observationWrap: return letters[lo % n].

Why it matters: Teaches that "search space" is a choice, not a given. Treat the matrix as one flattened sorted array of length m·n and map an index back with / and %. (Its cousin 240 has a weaker guarantee — see Pattern 2.)

"Each row sorted, and first of each row > last of previous — so it's globally sorted."
Before you solve
  • How do I convert a flat index k to (row, col)? (row=k/n, col=k%n.)
Hint ladder
Hint 1 · DirectionDon't search row then column. Pretend it's one array.
Hint 2 · Search spacelo=0, hi=m*n-1, exact match (Template B).
Hint 3 · Key observationAccess value as matrix[mid/n][mid%n].
Pattern 2

Rotated, peak & structural search

A · Pattern intuition

Here the array is not globally sorted, yet a local shape is still monotonic enough to throw away a half. This is the pattern people force into "classic" search and then get lost. It deserves its own head-space because the discard rule no longer comes from comparing a[mid] to a target — it comes from comparing a[mid] to a neighbor or an endpoint to figure out which half is "well-behaved."

Two shapes cover almost everything:

What triggers this pattern

"Rotated," "mountain," "peak," "first increases then decreases," or a sorted array that's been shifted. Also: you have no target but a structural landmark to find. That "no target, yet still binary-searchable" feeling is the tell.

B · Visual — rotated: one half is always clean

06
17
28
3·mid9
41
52
64
a[lo]=6 <= a[mid]=9 → left half [0..3] is sorted. Is target in [6,9]? If yes search left, else search right.

C · Templates

Peak (no target, compare to neighbor)

int lo = 0, hi = n - 1;
while (lo < hi) {
    int mid = lo + (hi - lo) / 2;
    if (a[mid] < a[mid + 1])
        lo = mid + 1;   // uphill → peak is right
    else
        hi = mid;       // downhill → peak here or left
}
return lo;             // a peak index

Rotated search (find target)

int lo = 0, hi = n - 1;
while (lo <= hi) {
    int mid = lo + (hi - lo) / 2;
    if (a[mid] == target) return mid;
    if (a[lo] <= a[mid]) {          // left sorted
        if (a[lo]<=target && target<a[mid]) hi=mid-1;
        else lo=mid+1;
    } else {                        // right sorted
        if (a[mid]<target && target<=a[hi]) lo=mid+1;
        else hi=mid-1;
    }
}
return -1;

D · How to derive it here

Peak: (3) monotonic property = "am I on the rising or falling slope?" — that flips once. (4) first index where you start falling = the peak → first-true style. Rotated: the monotonic fact isn't the array, it's "at any mid, exactly one side is sorted." Once you trust that, the check is just "is target inside the clean side's known range?" — if yes go there, otherwise the answer must be in the messy side.

Deep dive · Find Peak Element

162. Find any peak in [1, 2, 1, 3, 5, 6, 4]

A peak is bigger than both neighbors; edges count as −∞ outside. Climb toward the higher neighbor and a peak is guaranteed.

1
lo=0, hi=6 → mid=3, a[3]=3 < a[4]=5. Uphill. A peak must be right → lo=4.
2
lo=4, hi=6 → mid=5, a[5]=6 > a[6]=4. Downhill. Peak is here or left → hi=5.
3
lo=4, hi=5 → mid=4, a[4]=5 < a[5]=6. Uphill → lo=5. Now lo==hi.
01
12
21
33
45
56
64

Return index 5. No target, no sorted array — the slope direction was the monotonic signal all along.

Deep dive · Search in Rotated Sorted Array

33. Find target = 2 in [6, 7, 8, 9, 1, 2, 4]

1
lo=0, hi=6 → mid=3, a[3]=9. a[lo]=6 <= 9 → left sorted [6..9]. Is 2 in [6,9)? No. Discard left → lo=4.
2
lo=4, hi=6 → mid=5, a[5]=2 == target. Return 5.
06
17
28
39
41
52
64

The move that unlocks it: identify the sorted side first, then ask if the target fits in its known range.

Curated problems — foundation → synthesis

Foundation

Why it matters: The purest "binary search with no target." Once you can solve this, you've internalized that monotonicity — not sortedness — is the real precondition.

"Return any index that's a local max, in O(log n), array not sorted."
Before you solve
  • Why does climbing toward the taller neighbor guarantee hitting a peak?
  • Do I compare mid to a neighbor or to a target?
Hint ladder
Hint 1 · DirectionYou can't test "is mid a peak" and discard — but you can test the slope.
Hint 2 · Search spaceIndices [0, n-1]. Compare a[mid] vs a[mid+1].
Hint 3 · Key observationUphill → higher ground (a peak) is right. Downhill → peak is here or left.
Recognition

Why it matters: Same slope logic as 162 but with a guaranteed single peak. Good for confirming the template transfers when the problem "shape" is stated explicitly as a mountain.

"Strictly up then strictly down — find the summit index."
Hint ladder
Hint 1 · DirectionIdentical to Find Peak Element.
Hint 2 · Key observationBecause it's strictly unimodal, the slope never plateaus — no tie-breaking needed.
Variation

Why it matters: Finds the rotation pivot (the min) by comparing a[mid] to a[hi]. Teaches the "compare to an endpoint, not a target" reflex that makes rotated search click.

"Rotated sorted array, no target — just find the smallest element."
Before you solve
  • Should I compare mid to a[lo] or a[hi]? Why is a[hi] safer?
Hint ladder
Hint 1 · DirectionThe min is the one place where order "breaks." Steer toward the break.
Hint 2 · Search space[lo,hi] with hi=n-1; loop while lo<hi.
Hint 3 · Key observationIf a[mid] > a[hi], min is strictly right → lo=mid+1. Else hi=mid. Never mid-1 here — mid could be the min.
Interview-level application

Why it matters: The canonical rotated search and a top-tier interview frequency problem. Combines pivot logic with target containment — the synthesis of everything in this pattern.

"Sorted then rotated, distinct values, find target's index in O(log n)."
Before you solve
  • At each mid, which side is guaranteed sorted?
  • Given the sorted side's range, is target inside it or not?
Hint ladder
Hint 1 · DirectionDon't try to un-rotate. Work with the fact that one half is always clean.
Hint 2 · Search spacea[lo] <= a[mid] ⇒ left half sorted; otherwise right half sorted.
Hint 3 · Key observationOnly the sorted side lets you range-check target reliably. If target's in it, go there; else go the other way.
Challenging synthesis

Why it matters: Adds duplicates, which can break the "which side is sorted" test when a[lo]==a[mid]==a[hi]. Teaches the graceful degradation to O(n) worst case and why duplicates poison monotonicity.

"Same as 33 but values can repeat — decide true/false existence."
Hint ladder
Hint 1 · DirectionMost cases are identical to 33. Only one ambiguous case is new.
Hint 2 · Key observationWhen a[lo]==a[mid]==a[hi] you can't tell which side is sorted — shrink both ends (lo++, hi--) and retry.
Hint 3 · CostWorst case (all equal) degrades to O(n). That's expected, and a good thing to say out loud in an interview.

Why it matters: A structural cousin. Rows and columns sorted, but not globally — so flattening (74) fails. The O(m+n) staircase from the top-right corner is a "monotone elimination" that isn't literally binary search, and knowing the difference is the lesson.

"Rows sorted AND columns sorted, but no global order."
Hint ladder
Hint 1 · DirectionStart at top-right. That cell is the max of its row and min of its column.
Hint 2 · Key observationToo big → drop the column (go left). Too small → drop the row (go down). Each step deletes a full row or column.
Hint 3 · ContrastThis is elimination, not halving — O(m+n), not O(log). Don't force it into a log solution.
Pattern 3 · the one to master

Binary search on the answer

A · Pattern intuition

The mental leap that makes this hard: there is no array to search. You invent the search space. The array in the problem is just data; the thing you binary search is a range of possible answers — a speed, a capacity, a divisor, a maximum-allowed-sum. For each candidate answer mid, you ask a yes/no question — feasible(mid)? — and that question is usually a simple O(n) loop.

Why it works: as the candidate answer grows, feasibility flips once. Bigger eating speed → strictly easier to finish in time. Bigger ship capacity → strictly easier to ship in D days. That single-flip guarantee is your F…F T…T strip. You are back to "find first true," except the strip is a made-up range of answers.

1F
2F
3F
4T
5T
6T
7T
8T
Candidate answers 1…8. feasible(k) = "can I finish at speed k?" We want the smallest feasible = 4.
The recognition trigger — burn this in

"Minimize the maximum," "maximize the minimum," "smallest/largest X such that a condition holds," or "the answer is a number between X and Y, and if K works then everything bigger (or smaller) than K also works." The instant you can say "if K works, K±1 also works," you are here.

B · Designing the feasibility function — the real skill

Ninety percent of these problems are trivial once you have feasible(mid). Design it in three moves:

  • Fix the answer to mid and simulate. "Suppose the allowed value is exactly mid. Walk the data once and compute what that forces."
  • Reduce to one number and compare. Count the hours Koko needs, the days shipping takes, the bouquets you can make — then compare to the constraint (≤ H, ≤ D, ≥ m).
  • Confirm monotonic direction. Ask "if I nudge mid up, does the computed number move the right way?" If yes, you have your F…F T…T (or T…T F…F).
  • C · Two templates — min feasible vs max feasible

    Minimum feasible (F…F T…T) — Koko, ship, divisor, split

    int lo = minCand, hi = maxCand;
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if (feasible(mid)) hi = mid;   // works → try smaller
        else              lo = mid + 1;
    }
    return lo;   // smallest value that works

    Maximum feasible (T…T F…F) — magnetic force, max-min

    int lo = minCand, hi = maxCand;
    while (lo < hi) {
        int mid = lo + (hi - lo + 1) / 2; // upper mid!
        if (feasible(mid)) lo = mid;   // works → try bigger
        else              hi = mid - 1;
    }
    return lo;   // largest value that works
    The single most common infinite loop

    In the max feasible template you MUST bias mid upward with +1: mid = lo + (hi - lo + 1) / 2. Otherwise when hi = lo + 1 and feasible(mid) is true, lo = mid leaves lo unchanged → forever. The rule: if a branch does lo = mid, round mid up.

    D · How to derive it here

    (1) Search space = the range of possible answers, e.g. Koko speed [1, max(pile)]. (2) mid = a concrete candidate answer. (3) monotonic property = feasibility flips once. (4) most of these want the minimum feasible answer → min template. (5) feasible(mid) true → smaller might also work, keep mid (hi=mid). (6) false → mid too small, lo=mid+1. (7) stop at lo==hi. (8) return lo. The only per-problem work is bounds + feasible.

    Choosing lo and hi

    lo = the smallest answer that could conceivably be legal (speed 1, capacity = largest single item, divisor 1). hi = an answer guaranteed feasible (capacity = sum of all items, speed = biggest pile, divisor = max element). Set hi to something you can prove is always feasible — then the search can only shrink toward the true minimum.

    Deep dive · Koko Eating Bananas

    875. piles = [3, 6, 7, 11], H = 8 hours

    feasible(k) = sum of ceil(pile/k) ≤ 8. Search speed in [1, 11]. Want the minimum.

    1
    lo=1, hi=11 → mid=6. Hours = 1+1+2+2 = 6 ≤ 8 → feasible. Try slower: hi=6.
    2
    lo=1, hi=6 → mid=3. Hours = 1+2+3+4 = 10 > 8 → infeasible. Too slow: lo=4.
    3
    lo=4, hi=6 → mid=5. Hours = 1+2+2+3 = 8 ≤ 8 → feasible. hi=5.
    4
    lo=4, hi=5 → mid=4. Hours = 1+2+2+3 = 8 ≤ 8 → feasible. hi=4. Now lo==hi==4.
    1F
    2F
    3F
    4T
    5T
    6T
    …T
    11T

    Answer = 4 bananas/hour. Notice we never sorted the piles — the answer range was the sorted, monotonic thing.

    Curated problems — foundation → synthesis

    Foundation

    Why it matters: The gentlest "answer is a number, not an index." Search k in [0, x]; feasible = k*k <= x; you want the largest feasible. A clean first max-feasible search.

    "Largest integer whose square doesn't exceed x."
    Before you solve
    • Min-feasible or max-feasible? (Largest k that still fits → max.)
    • Watch k*k overflow — use long.
    Recognition

    Why it matters: The archetype. If you can derive this cold — bounds, feasibility, direction, return — you can derive most of the pattern.

    "The answer is a speed between 1 and max(pile); if speed K finishes in time, every speed > K does too."
    Before you solve
    • What are lo and hi? (1 and max pile.)
    • What does feasible(k) compute and compare against?
    • Min or max feasible?
    Hint ladder
    Hint 1 · DirectionDon't search the piles. Search the possible eating speeds.
    Hint 2 · Search space[1, max(piles)]; mid = a candidate speed.
    Hint 3 · Key observationfeasible(k)=Σ ceil(pile/k) ≤ H. Faster is always feasible → smallest true. Min-feasible template.

    Why it matters: Same shape as Koko but the feasibility loop is a greedy fill (order matters, can't reorder packages). Trains you to write a slightly richer feasible without changing the skeleton.

    "Smallest ship capacity so all packages ship within D days, in order."
    Before you solve
    • Why is lo = max(weight), not 1? (A day must hold at least the heaviest item.)
    • hi = sum(weights).
    Hint ladder
    Hint 1 · DirectionBinary search the capacity; simulate days for each candidate.
    Hint 2 · Search spacelo=max(w), hi=sum(w).
    Hint 3 · Key observationfeasible(cap) greedily packs left-to-right, counts days, checks ≤ D. Min-feasible.
    Variation

    Why it matters: Near-identical to Koko with a different feasible. Solving both back-to-back proves to yourself the skeleton is fixed and only the check changes.

    "Smallest divisor so that the sum of ceilings stays under a threshold."
    Hint ladder
    Hint 1 · DirectionIt's Koko with "hours" renamed "threshold."
    Hint 2 · Key observationfeasible(d)=Σ ceil(num/d) ≤ threshold; bigger divisor → smaller sum → min-feasible.

    Why it matters: The candidate is "days," and feasibility requires scanning for consecutive bloomed flowers. First problem where feasible needs a run-length idea — a step up in check design.

    "Fewest days such that m bouquets of k adjacent bloomed flowers exist."
    Hint ladder
    Hint 1 · DirectionBinary search the day; more days can only bloom more flowers → monotone.
    Hint 2 · Search space[min(bloom), max(bloom)].
    Hint 3 · Key observationfeasible(day) counts bouquets from maximal runs of flowers with bloom[i] ≤ day; check ≥ m.
    Max-feasible variation

    Why it matters: The mirror image — you maximize the minimum gap. Forces the max-feasible template with its upper-mid trap, so it locks in why lo=mid needs mid rounded up.

    "Place balls so the smallest pairwise distance is as large as possible."
    Before you solve
    • Sort positions first — feasibility needs order.
    • Am I finding the largest gap that still fits all balls? (Yes → max-feasible.)
    Hint ladder
    Hint 1 · DirectionBinary search the answer = minimum gap. Larger gap is harder to satisfy → T…T F…F.
    Hint 2 · Search space[1, max-min] after sorting positions.
    Hint 3 · Key observationfeasible(gap) greedily places balls ≥ gap apart, counts placed ≥ m. Use the upper-mid max template.
    Challenging synthesis

    Why it matters: The "minimize the maximum" flagship. Recognizing that the elusive answer (the largest subarray sum) is itself the thing to binary search — with a greedy feasibility count of splits — is the exact reasoning jump interviewers probe.

    "Split into k parts to minimize the largest part-sum → binary search that largest sum."
    Before you solve
    • What does mid represent? (A cap on any subarray's sum.)
    • lo = max(element), hi = sum(all). Why those?
    Hint ladder
    Hint 1 · DirectionStop thinking about where to cut. Binary search the allowed maximum sum.
    Hint 2 · Search spacelo=max(nums), hi=sum(nums); mid = a cap.
    Hint 3 · Key observationfeasible(cap) greedily counts how many pieces you need to keep every piece ≤ cap; feasible if pieces ≤ k. Min-feasible. (Identical skeleton to Capacity to Ship / Magnetic Force — this family is one idea.)
    Pattern 4

    Search over a value domain (count-based)

    A · Pattern intuition

    A close relative of Pattern 3, but the twist earns it a separate slot: the candidate mid is a possible value of the answer, and feasible(mid) is a counting question — "how many things in my data are ≤ mid?" This is how you binary search for a k-th smallest something without ever materializing all the candidates (there might be n² of them).

    The monotonic fact: count(≤ v) is non-decreasing in v. So "is count(≤ mid) ≥ k?" is a clean F…F T…T strip over the value range. The smallest v where that count reaches k is exactly the k-th smallest value — and it's guaranteed to be a real element because the count only jumps at real values.

    What triggers this pattern

    "K-th smallest / largest" over pairs, products, distances, or a matrix, where listing all candidates is too expensive, but counting how many are ≤ v is cheap (often via sorting + two pointers or per-row binary search). Also "median of …" (median = the (n/2)-th smallest).

    B · Visual — searching values, counting data

    v=11
    22
    34
    45
    57
    Top number = candidate value v. Bottom = count(≤ v). Want k=3rd smallest → first v where count ≥ 3, i.e. v=3.

    C · Template

    int lo = minValue, hi = maxValue;   // value range, not indices
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if (countLessEqual(mid) >= k) hi = mid;  // enough → shrink
        else                            lo = mid + 1;
    }
    return lo;   // the k-th smallest value
    Why lo is guaranteed to be a real element

    Even though we search over a continuous range of values, the count only increases when mid crosses an actual data value. The convergence point is therefore always a value that truly exists — you never return a "gap" number. This is the subtle correctness argument worth stating in an interview.

    D · How to derive it here

    (1) Search space = the value range [min, max] the answer could take. (2) mid = a candidate value. (3) monotonic property = count(≤ mid) only grows. (4) want the first value whose count reaches k → min-feasible. (5)/(6) count ≥ k → answer is mid-or-smaller; count < k → strictly bigger. (8) return lo. The whole difficulty is writing an efficient countLessEqual.

    Curated problems — foundation → synthesis

    Foundation

    Why it matters: The most surprising member — no sorting, no explicit search array. You binary search the value 1…n and count how many elements are ≤ mid. If the count exceeds mid, a duplicate hides at or below mid. A perfect "aha, the answer space is the numbers themselves."

    "n+1 numbers in range [1,n], read-only, find the one that repeats."
    Before you solve
    • What does count(≤ mid) > mid tell me by pigeonhole?
    Hint ladder
    Hint 1 · DirectionSearch the value range [1,n], not the array positions.
    Hint 2 · Search spacelo=1, hi=n; mid = a candidate value.
    Hint 3 · Key observationIf more than mid elements are ≤ mid, the duplicate is in [lo, mid] → hi=mid; else lo=mid+1.
    Recognition

    Why it matters: The clean template case. Search values in [matrix[0][0], matrix[n-1][n-1]]; count elements ≤ mid in O(n) using the row/column sortedness (staircase from bottom-left). Cements "binary search values, count with a cheap pass."

    "K-th smallest in a matrix with sorted rows and columns."
    Before you solve
    • How do I count elements ≤ mid in O(n) rather than O(n²)?
    Hint ladder
    Hint 1 · DirectionDon't heap it if you want the value-search lesson. Binary search the value.
    Hint 2 · Search spaceValue range from smallest (top-left) to largest (bottom-right).
    Hint 3 · Key observationCount ≤ mid by walking from bottom-left: move right when ≤ mid, up otherwise. Then min-feasible on count ≥ k.
    Challenging synthesis

    Why it matters: There are O(n²) pair distances — you can't list them. Sort, then binary search the distance value, counting pairs within distance mid via a sliding window. The definitive "count, don't enumerate" problem.

    "K-th smallest among all pairwise distances — far too many to build."
    Before you solve
    • After sorting, how do I count pairs with distance ≤ mid in O(n)?
    Hint ladder
    Hint 1 · DirectionBinary search the distance, not the pairs.
    Hint 2 · Search spaceSort first. lo=0, hi=max-min.
    Hint 3 · Key observationcount(≤ mid) with a two-pointer window over the sorted array; find smallest distance with count ≥ k.

    Why it matters: An implicit matrix you never build. Count values ≤ mid in the m×n table by summing min(mid/i, n) per row. Reinforces that "the data" can be a formula, not memory.

    "K-th smallest in an m×n times-table (i·j), which is huge."
    Hint ladder
    Hint 1 · DirectionBinary search the value 1…m·n. Never construct the table.
    Hint 2 · Key observationRow i contributes min(mid/i, n) entries ≤ mid; sum across rows, min-feasible on count ≥ k.
    Pattern 5

    Binary search as a helper

    A · Pattern intuition

    Here binary search is not the algorithm — it's a subroutine that keeps some auxiliary structure sorted so each update is O(log n) instead of O(n). The final answer is produced by the surrounding algorithm; binary search just does the fast lookup inside the loop. This is why LIS drops from O(n²) to O(n log n).

    The signature: you're maintaining a container that stays sorted by construction, and on each step you need "where does this new element belong / what's the first thing ≥ it." That question is a lower/upper bound — the boundary search from Pattern 1, used as a tool.

    What triggers this pattern

    You have an O(n²) DP or scan, and inside the inner loop you repeatedly search a monotone/sorted helper array. Whenever you catch yourself doing a linear scan over something that happens to stay sorted, a boundary search replaces it.

    B · Visual — the LIS "tails" array

    tails[i] = the smallest possible tail of any increasing subsequence of length i+1. It's always sorted. Each new number replaces the first tail ≥ it (or extends the array).

    len12
    len25
    len37
    new x=6→
    x=6: lower-bound finds index 2 (first tail ≥ 6 is 7). Replace 7 with 6 — same length, smaller tail, more room to grow later.

    C · Template

    int[] tails = new int[n];
    int len = 0;
    for (int x : nums) {
        // lower bound: first index with tails[i] >= x
        int lo = 0, hi = len;
        while (lo < hi) {
            int mid = lo + (hi - lo) / 2;
            if (tails[mid] >= x) hi = mid;
            else               lo = mid + 1;
        }
        tails[lo] = x;              // replace or append
        if (lo == len) len++;       // x extended the LIS
    }
    return len;
    Why binary search is valid here — and different from earlier patterns

    tails is provably strictly increasing at all times (a shorter subsequence always ends smaller), so lower bound is well-defined. Unlike Patterns 1–4, the binary search does not locate the final answer — len does. The search only maintains the structure that makes len cheap to update. Note: tails is not a real LIS, only its length is correct.

    D · How to derive it here

    Start from the O(n²) DP, spot the repeated inner scan, and ask "is the thing I'm scanning sorted?" If yes, the scan becomes a boundary search. For LIS specifically: use lower bound (first ≥ x) for strictly-increasing; switch to upper bound (first > x) if the problem allows equal elements (non-decreasing). That one predicate swap is the entire difference.

    Deep dive · Longest Increasing Subsequence

    300. nums = [10, 9, 2, 5, 3, 7, 18, 8]

    1
    x=10: tails empty → append. tails = [10]
    2
    x=9: first tail ≥ 9 is index 0 → replace. tails = [9]
    3
    x=2: first ≥ 2 is index 0 → replace. tails = [2]
    4
    x=5: none ≥ 5 → append. tails = [2, 5]
    5
    x=3: first ≥ 3 is index 1 (value 5) → replace. tails = [2, 3]
    6
    x=7: none ≥ 7 → append. tails = [2, 3, 7]
    7
    x=18: none ≥ 18 → append. tails = [2, 3, 7, 18]
    8
    x=8: first ≥ 8 is index 3 (value 18) → replace. tails = [2, 3, 7, 8]

    Length = 4. Every "replace" was a lower-bound search. The tails array [2,3,7,8] isn't the actual subsequence — but its length is the answer, computed in n·log n.

    Curated problems

    Foundation

    Why it matters: The definitive "binary search as a helper." Learn the patience-sorting / tails trick here and you'll recognize the shape everywhere.

    "Length of the longest strictly increasing subsequence, and O(n²) is too slow."
    Before you solve
    • What does tails[i] mean, and why is it always sorted?
    • Lower bound or upper bound? (Strict → lower.)
    Hint ladder
    Hint 1 · DirectionMaintain the smallest tail for each achievable length.
    Hint 2 · Search spaceBinary search within the sorted tails array, per element.
    Hint 3 · Key observationEach x replaces the first tail ≥ x, or extends the array. Answer = length of tails.
    Challenging synthesis

    Why it matters: LIS in disguise across two dimensions. The clever sort (width ascending, height descending on ties) reduces it to a 1-D LIS on heights — then reuse the exact helper from 300. Rewards pattern recognition over new machinery.

    "Nest envelopes (both dims strictly larger) — it's 2-D LIS."
    Before you solve
    • Why sort heights descending within equal widths? (To forbid equal-width nesting.)
    Hint ladder
    Hint 1 · DirectionSort to kill one dimension, then run LIS on the other.
    Hint 2 · Key observationSort width ↑, height ↓ on ties; run LIS (lower bound) on the height sequence.
    Hint 3 · TrapIf you sort height ascending on ties, two same-width envelopes could wrongly "nest."
    Pattern 6

    Partition binary search

    A · Pattern intuition

    The most specialized and the hardest to derive cold, so treat it as a named technique: you binary search where to cut one array so that the two arrays, split into a combined "left group" and "right group," satisfy a balance condition. You don't search for a value or a feasibility threshold — you search for a partition point.

    For the median of two sorted arrays: cut A at i and B at j so that the left halves together hold exactly half the elements, and every left element ≤ every right element. Fixing i forces j (their sizes must sum to half), so it's a 1-D search on i — over the smaller array to stay O(log min(m,n)).

    What triggers this pattern

    Two (or more) sorted arrays, and you need a global order statistic — median, k-th smallest across both — in logarithmic time. The words "two sorted arrays" + "median" + "O(log)" is almost always this exact problem.

    B · Visual — the correct cut balances four border values

    A:  a0  a1 | a2  a3      L1 = a1   R1 = a2
    B:  b0     | b1  b2  b3   L2 = b0   R2 = b1
    
    Correct cut  ⇔  L1 <= R2  AND  L2 <= R1
      too far right in A?  a1 > b1  → move A's cut left
      too far left  in A?  b0 > a2  → move A's cut right

    C · Template (sketch)

    // ensure A is the smaller array; total = m + n
    int lo = 0, hi = m;                 // cut position in A: 0..m
    while (lo <= hi) {
        int i = (lo + hi) / 2;          // elems from A on the left
        int j = (total + 1) / 2 - i;   // elems from B on the left
        int L1 = i==0?  MIN : A[i-1], R1 = i==m? MAX : A[i];
        int L2 = j==0?  MIN : B[j-1], R2 = j==n? MAX : B[j];
        if (L1 <= R2 && L2 <= R1) {        // balanced → answer at the border
            if (total % 2 == 1) return max(L1, L2);
            return (max(L1,L2) + min(R1,R2)) / 2.0;
        } else if (L1 > R2) hi = i - 1;   // A's cut too far right
        else              lo = i + 1;   // A's cut too far left
    }
    The details that sink people

    Search the smaller array (bounds correctness + speed). Use ±∞ sentinels for empty sides so border comparisons never index out of bounds. And j is fully determined by i — you never search two things.

    D · How to derive it here

    (1) Search space = cut positions 0..m in the smaller array. (2) mid/i = "how many of A's elements go left." (3) monotonic property = as i grows, L1 rises and R1 rises, so the two imbalance conditions each flip once. (4) you want the unique balanced cut. (5) L1 > R2 → too many big A-elements on the left → move left. (6) L2 > R1 → move right. (7) exact-match style loop. (8) return the median assembled from the four border values.

    Deep dive · Median of Two Sorted Arrays

    4. A = [1, 3], B = [2, 4, 5, 6] (total = 6, half = 3)

    A is smaller, search i in [0,2]. Need 3 elements on the left overall: j = 3 - i.

    1
    lo=0, hi=2 → i=1, j=2. L1=A[0]=1, R1=A[1]=3, L2=B[1]=3, R2=B[2]=5. Check L1≤R2 (1≤5) ✓ and L2≤R1 (3≤3) ✓. Balanced!
    2
    Left group = {1, 2, 3}, right group = {3, 4, 5, 6}. total is even → median = (max(L1,L2)+min(R1,R2))/2 = (3+3)/2. Median = 3.0.

    One iteration found the cut. The search never merged the arrays — it only hunted the balance point, in O(log min(m,n)).

    Curated problem

    Challenging synthesis

    Why it matters: The canonical partition search and a classic hard interview problem. Teaches "binary search the cut, not the value," plus disciplined edge handling with sentinels — a reasoning move no other pattern gives you.

    "Two sorted arrays, median in O(log(m+n)) — merging is too slow."
    Before you solve
    • Which array do I search, and why the smaller one?
    • Given cut i in A, what fixes cut j in B?
    • What are the two conditions that say "this cut is correct"?
    Hint ladder
    Hint 1 · DirectionDon't merge. Find where to split so left half = right half in size and order.
    Hint 2 · Search spaceCut position 0..m in the smaller array; j is derived from i.
    Hint 3 · Key observationCorrect when maxLeftA ≤ minRightB and maxLeftB ≤ minRightA. Use ±∞ for empty sides.
    Classifier

    Which binary search is this?

    Run a new problem through this top-down. The first "yes" that fits tells you the pattern and the template.

    Q0. Is anything monotonic — a value order, a slope, or a "if K works then K±1 works" feasibility? │ NO → not a binary search problem. Stop forcing it. │ YES ↓ Q1. Am I given (or can I cheaply build) a SORTED array, and I want a specific value / position / boundary? │ YES → do I want equality, or a boundary? │ • exact "is it here" → PATTERN 1, Template B │ • first/last/insert/≥/≤/> → PATTERN 1, Template A │ (this includes flattening a globally-sorted matrix) │ NO ↓ Q2. Is the array NOT globally sorted, but has a LOCAL SHAPE (rotated, or up-then-down / a peak)? │ YES → • find a peak / mountain summit → PATTERN 2 (slope vs neighbor) │ • find target/min in rotated → PATTERN 2 (which side is sorted?) │ NO ↓ Q3. Is there NO array to search — instead a RANGE OF POSSIBLE ANSWERS, and I can test one candidate with feasible(mid)? │ YES → • minimize a max / smallest valid → PATTERN 3, min-feasible │ • maximize a min / largest valid → PATTERN 3, max-feasible (upper-mid!) │ NO ↓ Q4. Do I want the K-TH SMALLEST / LARGEST / MEDIAN of something with too many candidates to list, but count(≤ v) is cheap? │ YES → PATTERN 4 (binary search the VALUE, count the data) │ NO ↓ Q5. Is binary search just a FAST LOOKUP inside a bigger algorithm that maintains a sorted helper structure (e.g. an O(n²) DP I want to speed up)? │ YES → PATTERN 5 (lower/upper bound as a subroutine; LIS-style) │ NO ↓ Q6. TWO sorted arrays + a global order statistic (median / k-th) in O(log) time? │ YES → PATTERN 6 (binary search the PARTITION / cut point)
    The 10-second version

    Sorted + find something → P1. Rotated/peak → P2. "Min the max / max the min / smallest valid" → P3. "K-th smallest of a huge candidate set" → P4. "Speed up an O(n²) with a sorted helper" → P5. "Median of two sorted arrays" → P6.

    Debugging

    Common failure modes

    Every one of these is a boundary or termination bug. Tiny examples show exactly where each bites.

    Mistake Why it breaks Fix
    while(lo <= hi) vs while(lo < hi) <= is for exact-match where you return mid inside. Use it with a converging boundary search and you re-test a size-1 window forever or overshoot. < hi for boundary/first-true (half-open). <= hi only when you return from inside on a hit.
    hi = mid vs hi = mid - 1 In first-true search, mid itself may be the answer. hi = mid - 1 would throw it away and return one-too-far. First-true keeps mid: hi = mid. Exact-match discards it: hi = mid - 1.
    lo = mid infinite loop When hi = lo + 1, lower-mid gives mid = lo; lo = mid never advances. Classic hang in max-feasible searches. If a branch does lo = mid, use upper mid: mid = lo + (hi - lo + 1) / 2.
    Computing mid = (lo + hi) / 2 lo + hi overflows int when both are near 2³¹ (First Bad Version with huge n). Always mid = lo + (hi - lo) / 2.
    Returning lo vs hi vs mid After a lo < hi loop, lo == hi so either is fine — but mid is stale. Guessing here means you didn't decide the return before coding. Boundary search → return lo. Last-valid → return stored ans or lo - 1. Decide at step 8 of the drill.
    Losing a candidate answer In "last true / max feasible," moving on without saving mid loses the best-so-far, and you return the neighbor. Either use the boundary-converging template (return lo/lo-1), or explicitly store ans = mid before moving.
    Wrong feasibility direction Writing feasible so the strip is T…T F…F but using the min-feasible (F…F T…T) template returns the opposite end. Print feasible(lo), feasible(mid), feasible(hi) once. Confirm the flip direction, then pick the matching template.
    Bad initial bounds hi too small excludes the true answer; lo too big never reaches it. E.g. Koko with hi = max/2 can miss the answer entirely. lo = smallest legal answer; hi = a value you can prove is always feasible (sum, max element, etc.).
    lower vs upper bound mix-up >= gives first occurrence; > gives one-past-last. Swap them and "count of x" or "last index of x" is off by the frequency of x. first ≥ target = lower (>=); first > target = upper (>); last ≤ target = upper − 1.
    Duplicates break "which side is sorted" In rotated search with repeats, a[lo]==a[mid]==a[hi] makes the sorted-side test ambiguous. Shrink both ends (lo++, hi--) in that one case; accept O(n) worst case.
    Integer overflow in feasibility k*k, sum of weights, or products can exceed int even when the answer fits. Use long inside feasible, or compare with division instead of multiplication.
    Accidentally breaking monotonicity Assuming a condition is monotone when it isn't (e.g. an unsorted input, or a feasibility that isn't actually one-directional) makes every discard invalid. State the flip explicitly ("bigger K ⇒ always feasible"). If you can't, binary search is the wrong tool.
    Tiny reproducer worth memorizing

    Array [5, 7], searching first index with a[i] >= 7. With hi = mid and lo < hi: mid=0, a[0]=5<7 → lo=1; loop ends, return 1. ✓   Now try the same with lo = mid in a max search and no upper-mid: mid stays 0 forever. That's the whole infinite-loop bug in two elements.

    Pre-interview

    5-minute revision sheet

    Read this on the train. If a row's Signal matches your problem, the rest of the row is your plan.

    Pattern Signal Search space Invariant Movement rule Return
    1 · Classic & boundary Sorted array; want value / position / first-last / insert. Indices [0,n) or [0,n]. Everything left of lo is false; answer in window. Exact: mid±1, return on hit. Boundary: true→hi=mid, false→lo=mid+1. lo (boundary) or mid/-1 (exact).
    2 · Rotated / peak Not globally sorted; rotated, mountain, or "find a peak." Indices. One side is always sorted / slope points to answer. Peak: uphill→lo=mid+1, else hi=mid. Rotated: identify sorted side, range-check target. lo (peak/min) or index/-1 (target).
    3 · On the answer "Min the max", "max the min", "smallest/largest valid K"; if K works, K±1 works. Range of possible answers [minCand,maxCand]. feasible flips once over the range. Min: feasible→hi=mid, else lo=mid+1. Max: feasible→lo=mid (upper-mid!), else hi=mid-1. lo = smallest/largest feasible.
    4 · Value domain K-th smallest/largest/median of a huge candidate set; count(≤v) is cheap. Value range [minV,maxV]. count(≤v) non-decreasing. count≥k→hi=mid, else lo=mid+1. lo = k-th value (a real element).
    5 · Helper Speeding an O(n²) scan; a sorted helper you keep updating. Inside the helper array. Helper stays sorted by construction. Lower/upper bound of x; replace or extend. Not the search — the outer var (e.g. LIS length).
    6 · Partition Two sorted arrays; median / k-th across both in O(log). Cut position 0..m in the smaller array. Left group size fixed; seek all-left ≤ all-right. L1>R2→hi=i-1; L2>R1→lo=i+1. Median from the 4 border values.
    The interview checklist (say it out loud)

    Before writing a single line:

    1. "What's monotonic here?" — name the flip.
    2. "What am I searching over?" — indices, answers, values, or a cut.
    3. "What does mid mean?" — finish the sentence.
    4. "First valid or last valid?" — picks the template.
    5. "Prove each discard." — why is that half safe?
    6. "Loop condition & return." — decide now, not after.
    The three templates, from memory

    First-true: lo=0,hi=n; while(lo<hi){mid; check?hi=mid:lo=mid+1} → lo

    Exact: lo=0,hi=n-1; while(lo<=hi){==?return; <?lo=mid+1:hi=mid-1} → -1

    Max-feasible: mid=lo+(hi-lo+1)/2; ok?lo=mid:hi=mid-1 → lo

    The one sentence that fixes "I understand it after seeing the answer"

    Don't ask "what's the code." Ask "what fact lets me delete half?" — name that fact in words first, and the loop, the boundaries, and the return all follow from it.