Avismara Hugoppalu
← All musings

Data Structures & Algorithms

Binary search has nothing to do with sorted arrays

It searches a monotonic answer space, not an array. Why min/max isn't always DP, and the isValid + monotonic-boundary checklist that tells you when to bisect a range.

June 8, 2026 · 7 min read

TL;DR
Binary search is filed under "sorted arrays," and that filing hid half its uses from me for years. The real prerequisite isn't an array — it's a monotonic yes/no question. When a problem asks for the smallest (or largest) value such that some condition holds, and the condition flips exactly once as the value grows, you binary-search the answer space directly. You're not building the answer out of subproblems; you're locating the boundary between fail and pass. This is part of a series on reaching for the right pattern instead of reflexive DP.

For a long time I had a wrong rule in my head, and it was a reasonable wrong rule: if a problem asks me to minimize or maximize something, that's optimization, and optimization means greedy or DP. Min/max was build-the-answer territory. So when I'd hit a problem like "what's the minimum speed such that you finish in time," I'd start hunting for a recurrence — what's the subproblem, what's the state — and find nothing that fit. There was no clean subproblem because the problem wasn't built that way.

The thing that dissolved the confusion wasn't a new trick. It was someone drawing a line I hadn't known was there: the line between building an answer and searching for one. Min/max sits on both sides of that line. That shared keyword is exactly the trap.

Build versus search

Here's the distinction, as sharply as I can put it.

In greedy and DP, the answer is constructed. You combine the answers to smaller pieces — subproblem results, a running best — and the structure of the problem produces the value. You don't know the answer until the construction finishes; the construction is the computation. (Greedy is the same construction with a proof that local choices are safe.)

In binary search on the answer, nothing gets constructed. The answer already lives somewhere in a known range — some speed between 1 and the biggest pile, some capacity between the heaviest item and the total weight. You don't compute it. You guess a value and ask a cheap yes/no question: does this candidate work? Then you guess again, narrowing the range, until the range collapses onto the boundary.

The distinction. Greedy/DP: the answer is built by combining subproblem answers — the structure produces it. Binary search on the answer: the answer lives in a range, and you can write a cheap isValid(candidate) checker — so you search for it instead of building it.

The mental shift is that "minimize X" stops being "compute the smallest X" and becomes "find the boundary between the X values that fail and the X values that pass." Those are different verbs. One produces a number. The other locates a frontier.

The condition that makes search legal

A binary search needs order. In a sorted array, the array gives you the order for free — that's why the technique got filed there. But the order you actually need isn't in the data. It's in the answer.

What makes the answer space searchable is monotonicity of the validity check. Picture every candidate value laid out on a line and label each one pass or fail:

candidate:   1   2   3   4   5   6   7   8
isValid:     N   N   N   N   Y   Y   Y   Y
                          ^
                          the boundary you want

If isValid flips from N to Y exactly once and never flips back, the candidates are effectively sorted by a single boolean — and a single boolean in sorted order is the only thing binary search ever needed. There was never a real requirement for an array. The array was just the most common place this property showed up.

If the column were N Y N Y Y N, binary search is meaningless: there's no single boundary to find, and bisecting tells you nothing about the halves you discard. Monotonicity is not a nicety here. It's the entire license to search.

The three-question checklist

This is the test I locked in, and I run it before writing a line:

  1. Does it ask for a min or max?
    The phrasing to listen for is "minimum/maximum X such that a condition holds." That "such that" is doing real work — it means the value is gated by a condition, not assembled from parts.
  2. Can I write isValid(candidate) cheaply?
    Given a fixed candidate value, can I check yes/no whether it satisfies the condition, typically in O(n)? If checking one candidate is itself hard, this pattern won't save me.
  3. Is isValid monotonic across the range?
    If a candidate passes, does every larger (or every smaller) candidate also pass? If yes, there's exactly one boundary, and it's findable by bisection.

All three yes, and you binary-search the answer space. Notice what the checklist does: it never asks you to find a recurrence or prove an exchange argument. It asks whether you can recognize a valid answer when handed one — a strictly easier job than producing one. That asymmetry is the whole payoff. Verifying is cheaper than constructing, and this pattern cashes that in.

Build (greedy/DP)Search (binary search on answer)
What you writea recurrence / a safe-choice rulean isValid(candidate) checker
What the answer isconstructed from subproblemslocated in a known range
Core propertyoptimal substructure / safe local choicemonotonic yes/no boundary
Hard partdefining the state / the proofthe off-by-one in the bisection

A worked example

Take the eating-speed problem: there are piles of bananas and a fixed number of hours, and you want the smallest integer speed such that every pile is finished within those hours. At one banana per hour you'll surely run out of time; at a speed equal to the largest pile you'll clearly finish; the answer is somewhere between.

Run the checklist. Min? Yes. Cheap checker? Given a speed, I can compute the hours that speed requires in one pass over the piles. Monotonic? Yes — a faster speed never needs more hours, so once a speed is fast enough, every faster speed is too. Three yeses.

The checker just sums up, for each pile, how many hours that pile takes at the given speed:

func canFinish(piles: [Int], speed: Int, within hours: Int) -> Bool {
    var needed = 0
    for pile in piles {
        // ceil(pile / speed): a partial pile still costs a full hour
        needed += (pile + speed - 1) / speed
    }
    return needed <= hours
}

That single ceil line is the only non-obvious part: a pile of 7 at speed 3 is three hours, not two, because the leftover banana still occupies an hour. Now bisect the speed range, looking for the leftmost speed that passes:

func minSpeed(piles: [Int], hours: Int) -> Int {
    var lo = 1
    var hi = piles.max()!
    while lo < hi {                       // converge to a single survivor
        let mid = lo + (hi - lo) / 2
        if canFinish(piles: piles, speed: mid, within: hours) {
            hi = mid                      // mid works; it might be the answer, keep it
        } else {
            lo = mid + 1                  // mid fails; the answer is strictly larger
        }
    }
    return lo
}

The exact same skeleton solves the ship-within-D-days problem: candidate is a daily capacity, isValid sweeps the items counting how many days that capacity needs, monotonic because more capacity never costs more days. Different problem, identical machinery — because both are "minimum X such that a condition holds" over a monotonic check. Once you see the shape, the surface story stops mattering.

Binary search on the answer · eating speedstep 1/5
1
lo
2
3
4
5
6
mid
7
8
9
10
11
hi
mid = 6hours(mid) = 6 8valid → search lowerrange [1, 11]

Validity is monotonic — once a speed works, every higher speed works. So the answer is the boundary between red and green, and we bisect to it.

The honest sharp edge

Boundary-finding binary search is where off-by-one bugs live, and I won't pretend otherwise. The classic mid == target exit doesn't apply — there's no single target, there's a frontier, and you want the first cell on the pass side. Three details decide whether you land on it:

Before
while lo <= hi (looks for an exact hit)
set hi = mid - 1 on a passing candidate
return mid from inside the loop
After
while lo < hi (converges on a boundary)
set hi = mid on a passing candidate — it might be the answer
return lo after the loop

The load-bearing choice is hi = mid, not hi = mid - 1, when a candidate passes. A passing candidate is itself a contender for the smallest passing value, so you must not discard it — you shrink the range up to and including it. Pair that with lo < hi and computing mid with lo + (hi - lo) / 2, and the window closes onto exactly the boundary. Get the asymmetry wrong and you either skip the answer or loop forever. There's no shame in tracing it by hand on a three-element range; I still do.

The takeaway
Binary search isn't about arrays — it's about a monotonic yes/no boundary, and the array was only ever the most common place that boundary lived. When a problem says "minimum X such that a condition holds," stop trying to build X. Write the cheap isValid(candidate) check, confirm it flips once, and bisect the range. You're not computing the answer; you're asking a yes/no question about candidates and locating the line between fail and pass. The only thing left to get right is the off-by-one.

Sibling patterns in this series: greedy and DP — the two build-the-answer tools this one is so easily confused with.

Problems to practice

Problems to practice