Binary Search on the Answer
Binary search locates the boundary of a monotone predicate in probes; sorted arrays are only one instance. We first establish the half-open `while (lo < hi)` template for and , then generalize to "binary search on the answer": whenever feasibility is monotone in a numeric parameter, we binary search the parameter itself, calling a feasibility check at each step.
╌╌╌╌
We have seen binary search as the canonical divide-and-conquer search: a sorted
array of keys, a target , and a halving loop that finds (or proves it
absent) in comparisons. That framing is correct but narrow. What
binary search actually needs is a
monotone predicate: a boolean test that is false up to some boundary
and true from there on. Sorted membership is just one instance, where
. Once we see binary search as locating the
boundary of a monotone predicate, we can search ranges that no array ever
materialises, the technique known as binary search on the answer.
Recap: binary search on a sorted array
Fix a sorted array and a target . The loop maintains an interval guaranteed to contain if it is present at all.
Because the interval length drops geometrically, the loop runs times.
The plain does occur?
version is easy; the subtle and far more useful
versions are the boundary queries.
lower_bound and upper_bound
Two queries answer almost every practical question about a sorted array:
- is the first index with .
- is the first index with .
Their difference is the count
of elements equal to ; itself is the insertion point
that keeps sorted. Both are boundary searches over the monotone predicate
(respectively ): a sorted array makes
go false, …, false, true, …, true exactly once.
The reliable template is half-open and uses lo < hi, never lo <= hi. We
search for the smallest index in at which the predicate holds, treating
index as a virtual past the end
sentinel that is always feasible.
- 1
- 2half-open: hi is past-the-end
- 3while do
- 4floor, and overflow-safe
- 5if then
- 6mid may be the answer
- 7else
- 8mid infeasible
- 9return: boundary
Two details make this correct, and getting either wrong is the classic off-by-one bug:
- The interval is half-open, as a search space but inclusive as
an answer. We initialize , not , because the answer can be
no element is ,
i.e. index . The loop's exit then names a valid boundary in . - The mid uses and the two branches are asymmetric. When holds we set (not ), because is itself a candidate boundary. When fails we set , because is now known-infeasible and must be excluded. With a floored mid, always, so strictly shrinks the interval and the loop cannot spin forever. For , change the test to ; nothing else moves.
One caution on the mid expression. Written as in a
32-bit integer type, the sum overflows as soon as exceeds :
with (legal indices into a large byte array) the sum
wraps negative and the midpoint
lands outside the interval entirely. The form
computes the same value, but the
intermediate never exceeds the interval width, so it cannot overflow
while holds. This bug sat in production binary searches for decades
because it only fires on arrays longer than a billion elements.
The template, traced
Run on
The first index with is , and the loop finds it in three probes:
| iter. | ? | update | ||||
|---|---|---|---|---|---|---|
| 1 | yes | |||||
| 2 | no | |||||
| 3 | no | |||||
| exit | return |
Iteration 3 is the critical case: the interval holds one
untested candidate plus the feasible frontier, floors to , and the
false branch moves past it. In this two-element configuration a wrong
update rule spins forever (the bug taxonomy below returns to it).
The same array answers the counting question. tests instead: it probes (no, ), then (no, ), then (yes, ), and returns . The number of s is , computed without ever scanning the run of equal keys: counting duplicates stays even when the run has length .
The generalization: searching a monotone predicate
Nothing in the loop above inspected the array except through . Abstract it away. Let be monotone:
The pattern in the definition is forced, not assumed.
Monotonicity says the true-set is upward closed: if it
contains it contains everything above . An upward-closed subset of
is a suffix, so it is either empty, or exactly
for . There is one
transition or none, and the none
case is why the template carries an
always-feasible sentinel at : it guarantees the true-set is nonempty, and
no real answer exists
comes back encoded as the sentinel itself.
If is monotone and computable, we can find by binary search over the numeric range , with no array at all. This is binary search on the answer: we are searching the space of candidate answers, and the only thing that makes it work is that feasibility is monotone in the answer. Monotonicity is what makes the search sound and complete for the threshold: the single transition means the boundary we return is the true and no other transition can be mistaken for it.
The cost is uniform across every application:
We pay probes, because each iteration replaces the interval width by at most , so after probes the width is at most , and the loop stops when it reaches . The logarithm is what makes the technique scale: a range of costs probes, and a range of costs . Sixty evaluations of a feasibility check settle a question over an answer space no machine could enumerate. Replacing the array access by an arbitrary monotone test is the entire idea.
Worked examples
In each case the work is the same three steps: name the answer parameter and its range , name the monotone predicate , and write the feasibility check. The binary search loop never changes.
Koko eating bananas
Koko has piles and hours; at speed she clears hours on pile . Minimize such that she finishes within hours.
- Answer parameter: the speed , an integer in .
- Predicate: ,
can finish in hours.
- Monotonicity: a larger never increases any term ,
so is monotone increasing in (
falsefor tiny speeds,trueonce fast enough). Binary search the smallest feasible .
Each check is , the range is , so the cost is .
The table above is what the predicate looks like; the search never builds it. Run the loop on this instance. The range is , and is a legitimate always-feasible sentinel: at speed every pile takes exactly one hour, so the total is . Four probes suffice (), and each one evaluates the actual sum of ceilings:
| probe | ? | update | |||
|---|---|---|---|---|---|
| 1 | yes | ||||
| 2 | no | ||||
| 3 | yes | ||||
| 4 | yes | ||||
| exit | return |
Probes 3 and 4 happen to compute the same total, hours at both and ; the check uses the total only through the comparison, and the search still needs probe 4 to learn that is feasible while is not. Out of eleven candidate speeds, only four were ever examined.
Capacity to ship / split array largest sum
These two problems are the same problem. Given an array and a count (days /
parts), partition it into contiguous groups to minimize the maximum group
sum. (Capacity to ship within days
reads the array as package weights;
Split array largest sum
reads it as integers, with identical structure.)
- Answer parameter: the cap on a group's sum, in . (You cannot go below the largest single element; you never need to exceed the whole sum.)
- Predicate:
the array can be split into contiguous groups, each with sum .
- Feasibility check (greedy, ): sweep left to right, accumulating into the current group; whenever adding would exceed , close the group and start a new one at . The number of groups this greedy uses is the minimum possible for cap , so holds iff that count is .
A larger only ever merges groups, so the group count is non-increasing in : is monotone, and we binary search the smallest feasible . Cost: .
On with the range is , and the search runs the greedy sweep four times:
| probe | greedy groups | count | ? | update | ||
|---|---|---|---|---|---|---|
| 1 | yes | |||||
| 2 | no | |||||
| 3 | yes | |||||
| 4 | no | |||||
| exit | return |
Probes 2 and 4 fail for the same structural reason: once , the packages and can no longer share a group, and the greedy is forced to three. The returned optimum is itself a sum of a contiguous run, necessarily: is a step function of whose value can only change at caps equal to some contiguous-run sum, so the smallest feasible cap always lands on one. The binary search does not exploit this; it simply cannot return anything else.
Integer square root
A pure-numeric instance with no array in sight: given , compute , the largest with .
- Answer parameter: , in (or tighten to ).
- Predicate: here the natural test is monotone the other
way, namely
true, …, true, false, …, so we want the lasttrue. Search the first with via the standard template and subtract one; or flip the comparison and keep thelargest feasible
form.
The check is , so the integer square root costs , and the same shape computes any .1
Traced for with the flip: search the first with over (any with works as the sentinel; ). Probe : fails, . Probe : holds, . Probe : holds, . The loop exits at , and .
When monotonicity fails
The precondition is easy to violate with an innocent-looking change of
predicate. Ask Koko's question with equality instead of inequality:
, she finishes in exactly hours.
On the hour totals for
are , so reads
two transitions. Feed this to the smallest-feasible template:
the first probe is , is false, and the template concludes the
boundary lies to the right, setting . Both true cells are gone.
Every later probe is false too, so the loop drifts up to the sentinel and
returns , a speed that does not even satisfy . Nothing inside the loop
misbehaved; the precondition was false, so the invariant everything below is infeasible
broke at the very first update.
The repair is standard: search a monotone relaxation and test afterwards.
Here, find the smallest with (the original monotone
predicate, giving ), then check whether happens to
hold. More generally, before trusting any answer-space search, write the
one-line monotonicity argument (increasing the parameter only relaxes the constraint, so a feasible answer stays feasible
) and confirm the sentinel
is feasible by construction. If either fails, the loop still terminates and
still returns something; it just returns garbage.
Correctness and termination
Every variant rests on one invariant, stated here for the smallest feasible
half-open template ( inclusive as an answer):
To see the loop bug fire, suppose we want the last true (the
integer-square-root shape) and, improvising, keep the floored with the
updates on true and on false. On the
two-element interval , with true: , the true branch assigns , and the state is
exactly what it was. The loop runs forever, and it does so only when the search
has already narrowed to two candidates, which is why the bug survives casual
testing: small hand-checked examples that happen to exit earlier look fine.
The correct last true
mirror uses the ceiling midpoint:
- 1invariant: true, everything above false
- 2while do
- 3ceiling: always
- 4if then
- 5mid feasible, may be the answer
- 6else
- 7mid known-infeasible
- 8return
Now whenever , so both branches strictly shrink the
interval; the roles of the floor and ceiling are symmetric to the roles of
and . The rule of thumb: whichever side the update
keeps ( or ), round away from that side.
Floor pairs with ; ceiling pairs with . This form
returns the largest feasible value directly, which is what the integer square
root wanted before we flipped it into a first-false search.
Binary search on a real interval
When the answer is a real number rather than an integer (minimize a continuous radius, a rate, a time), the boundary need not be representable exactly, so we run parametric search: the same loop on a real interval, stopping after a fixed number of iterations or once .
- 1
- 2repeat times:error
- 3
- 4if then else
- 5return
Each iteration halves the interval, so iterations reach absolute error ; solving for the iteration count that reaches a tolerance gives
The numbers stay small even for extravagant demands: an interval of width pushed down to needs iterations. There is no bookkeeping because we never need the exact integer boundary, only an -close one.
Preferring a fixed over the loop condition while (hi - lo > eps) matters
for correctness. A double carries significand bits, so once the interval is a
few units in the last place wide, rounds to or and the
interval stops shrinking; if
is below that granularity, the eps-condition never becomes
false and the loop hangs. A fixed (around for doubles, comfortably
past machine precision) is immune by construction. As always, the predicate's
monotonicity is the real precondition: if is monotone over the loop
converges to its boundary; if is not monotone, binary search is simply the
wrong tool, since there may be several transitions and no guarantee which one
we land on.3
Parametric search, bisection, and decision vs. optimization
Binary search on the answer
is the discrete, hand-rolled special case of a
broad optimization paradigm. When the feasibility check is itself a shortest-path
or flow computation, the technique is parametric search (Megiddo, Applying
Parallel Computation Algorithms in the Design of Serial Algorithms, JACM 1983),
which replaces the numeric probe with a simulation of the check run on the
unknown optimum, and is the classical route to problems like the minimum-ratio
cycle and the -th smallest distance. The everyday version — pick a value,
run a Boolean feasibility test, halve the range — is what this lesson
does, and it is worth recognizing that the two are the same idea at different
levels of sophistication.
The continuous analogue, bisection on a monotone real predicate, is a
root-finding method: to solve for monotone , binary search the
sign of . Bisection converges linearly (one bit per step), which is why
numerical libraries pair it with faster-but-fragile methods — Brent's method
(Brent, Algorithms for Minimization Without Derivatives, 1973) falls back to
bisection whenever the superlinear step would leave the bracketing interval, so
it keeps bisection's guaranteed convergence while usually running faster. The
binary search on a real parameter until the interval is small enough
pattern
in this lesson is bisection with an explicit tolerance, and the same caution
applies: it needs a genuine sign change (a genuine monotone predicate) bracketed
at the endpoints, or it converges to nothing meaningful.
Finally, the monotone-predicate framing connects binary search to decision vs.
optimization. Many optimization problems are solved by reducing them to a
sequence of decision (is a solution of quality feasible?
) problems and
binary searching ; the reduction is efficient precisely when the decision
version is polynomial and feasibility is monotone in . That is the same move
that turns an NP optimization problem into its NP decision counterpart, and in
the tractable case it is this lesson's technique verbatim.
Takeaways
- Binary search locates the boundary of a monotone predicate in probes; the sorted array is just the special case .
- Memorise one template. The half-open
while (lo < hi)form with a floored mid, on feasible and on infeasible, returning , computes and without off-by-one errors. - Binary search on the answer: when feasibility is monotone in a numeric parameter, binary search the parameter and call a feasibility check at each step, for total cost .
- Recipe for each problem: name the answer range , the monotone predicate , and an efficient check. Koko (speed; sum of ceilings), ship/split (max-group cap; greedy partition in ), integer square root () all fit this mold.
- The correctness invariant keeps infeasible and feasible; floored mid plus asymmetric updates guarantee termination. Mixing the closed and half-open templates is where the classic bugs live, and the infinite-loop variants all fire on the two-element interval. Rounding rule: floor pairs with , ceiling with .
- Use binary search on the answer when checking is easy but solving is
hard: evaluating
is cap enough?
is a linear greedy sweep, while computing the optimal directly is not. The search converts a verifier into an optimizer at a factor. - Verify monotonicity before trusting the output. Equality-style predicates have two transitions and send the search to garbage without any visible failure; search the monotone relaxation ( instead of ) and test the boundary afterwards. Confirm the sentinel is feasible by construction.
- For a real-valued answer, run parametric search: a fixed iteration count
rather than an
epsloop condition, which can hang at floating-point granularity. Monotonicity of is the only precondition that matters.
Footnotes
- Erickson, Ch. 1 — Recursion: binary search as recursive boundary-finding, including pure-numeric instances such as integer roots. ↩
- CLRS, Ch. 2 — Binary search (Exercise 2.3-5): the sorted-array search and its boundary (insertion-point) variants. ↩
- Skiena, §4.9 — Binary Search and Related Algorithms: searching a monotone predicate over a numeric range (
binary search on the answer
) and one-sided/parametric variants. ↩
╌╌ END ╌╌