Binary search finds the boundary of a monotone predicate in evaluations. Searching a sorted array is the least interesting use. The big ones are binary searching the answer of an optimisation problem, and descending a data structure.
One template, one invariant
// predicate is F F ... F T T ... T on [lo, hi); find the first T (or hi if none)
ll lo = LO, hi = HI; // invariant: pred false on [LO, lo), true on [hi, HI)
while (lo < hi) {
ll mid = lo + (hi - lo) / 2; // floor division for lo >= 0; use (lo + hi) >> 1 with negatives
if (pred(mid)) hi = mid; else lo = mid + 1;
}
// lo == hi == first true- For the last true in a
T...T F...Fpredicate, search the first false and subtract one. - Alternative form that some people prefer: keep
lalways false andralways true, loopwhile (r - l > 1), and the answer isr. Pick one form and never mix them. himust be a provable bound. Too small silently clamps the answer; too large can overflow insidepred(for examplemid * midwithmidnear ).(lo + hi) / 2rounds toward zero, which breaks the invariant when the range includes negatives.lo + (hi - lo) / 2or(lo + hi) >> 1(arithmetic shift is floor) are safe.
Proof: the template terminates and is correct
Invariant maintained. Before the loop,
predis (vacuously) false on and true on is not required yet — the real starting invariant is just “false belowlo, true at or abovehi” withlo=LO,hi=HIbounding the unknown region. Each step computesmidstrictly betweenloandhi(sincelo<hi), and: ifpred(mid)is true, settinghi=midkeeps “true at/abovehi” (mid itself is now confirmed true, and the invariant extends down to it) without touching the false region; if false, settinglo=mid+1keeps “false belowlo” (mid is now confirmed false, extending the false region up through it). Monotonicity ofpred(false-then-true) is exactly what guarantees no update ever has to revise an earlier region.
Termination.midstrictly betweenloandhimeans eitherhidecreases tomid<hiorloincreases tomid+1>lo, sohi-lostrictly decreases every iteration (it does not just stay the same), hence the loop runs times and terminates withlo==hi.
Correctness at exit. Whenlo==hi, the invariant “false belowlo, true at/abovehi” withlo=hisays exactly: false on , true on — solois the first true, exactly as claimed (orHIitself ifpredis false everywhere on , sincehinever decreases past a true position).
Bitwise descent is the same search written over bits. It finds the largest with pred(x) true when the predicate is T..T F..F, and it’s the shape used by Fenwick and binary-lifting descents:
ll x = 0;
for (int b = K - 1; b >= 0; b--) if (pred(x | 1LL << b)) x |= 1LL << b;STL
| Call | Returns |
|---|---|
lower_bound(all(a), x) | first |
upper_bound(all(a), x) | first |
upper_bound - lower_bound | count of |
prev(lower_bound(...)) | last (check begin) |
partition_point(all(a), pred) | first element where pred is false (pred must be T..T F..F) |
ranges::lower_bound(a, x, {}, &T::key) | projection onto a field |
On set/map use the member s.lower_bound(x). The free function is on non-random-access iterators.
Real-valued search
double lo = 0, hi = 1e9;
for (int it = 0; it < 100; it++) { // fixed count: always terminates
double mid = (lo + hi) / 2;
(pred(mid) ? hi : lo) = mid;
}while (hi - lo > eps) can loop forever: when values are around a double can’t represent a difference of . 100 iterations exhaust double precision for any range. For a relative-precision answer spanning many magnitudes, search on or bisect the bit pattern of the double.
Binary search on the answer
If “can we achieve ?” is monotone in , optimisation becomes feasibility. The statement phrases are “minimise the maximum”, “maximise the minimum”, “minimum time such that”, “largest such that”.
| Problem | Feasibility check for |
|---|---|
| split array into parts minimising the max sum | greedy: cut when the running sum would exceed |
| place points maximising the minimum gap | greedy from the left |
| -th smallest pairwise sum / distance / product | count pairs with two pointers, per check |
| -th smallest in an row/column-sorted matrix | staircase count, |
| -th smallest of with signs | count with two pointers on sorted positive/negative parts |
| max average subarray with length | subtract , then check for a non-negative sum of length using prefix minima |
| max median of a subarray with length | map to by comparing with , same check |
| minimum time for machines to make items | (watch overflow: cap the sum) |
| minimum largest edge so that the graph is connected / has a perfect matching / 2-SAT holds | rebuild with edges |
| minimum radius / time with a geometric condition | real binary search + check |
| minimum with a DP inside | the DP becomes simpler with a fixed threshold |
Combinations worth recognising: binary search + 2-SAT (separation constraints), binary search + max flow (scheduling by a deadline), binary search + matching (bottleneck assignment), binary search + shortest path (minimise the maximum edge, or a parametric cost), binary search + DP (the check re-runs a or cheaper DP with the threshold baked in — often simpler than the unconstrained DP, since fixing removes a dimension).
Fractional programming
To maximise () over a feasible set, binary search and check , which is a linear objective. Dinkelbach’s iteration is faster in practice: where is optimal for . It converges superlinearly, usually in 5-10 rounds.
Uses: minimum mean cycle, optimal ratio spanning tree, maximum density subgraph (each check is a min cut), “best average” knapsack (choose items maximising : sort by ).
When the check costs a log too
Total cost is . If both the outer search and the check are , consider descending a structure instead (below). If there are many independent searches over the same sequence of updates, use parallel binary search.
Searching inside a structure
| Structure | Descent |
|---|---|
| Fenwick tree | find the smallest prefix with sum in by bitwise descent, instead of |
| segment tree | max_right(l, pred) / min_left: walk down to the first index where the aggregate stops satisfying pred |
| treap / order-statistic tree | descend by subtree sizes for -th or rank |
| tree ancestors | binary lifting: jump while the condition holds (first ancestor with value , LCA) |
| wavelet tree / persistent segment tree | -th smallest in a range by descending on counts |
| convex hull / sorted angles | binary search the tangent or the sector |
int bitLowerBound(ll k) { // Fenwick t[1..n] (non-negative values), smallest i with prefix(i) >= k
int pos = 0;
for (int pw = 1 << __lg(n); pw; pw >>= 1)
if (pos + pw <= n && t[pos + pw] < k) pos += pw, k -= t[pos];
return pos + 1;
}Proof:
bitLowerBoundis correctStandard Fenwick indexing means
t[pos+pw], whenposhas its lowestpw-and-below bits all zero (an invariant maintained by only ever adding powers of two in decreasing order), stores the sum of the range — exactly the next elements’ total starting afterpos. The loop tries every power of two from largest to smallest, in the same order a binary representation of the answer’s offset fromposwould be built: if the block sums to less than the remaining target , that whole block can be safely skipped (it cannot contain the crossing point, since even including all of it doesn’t reach ) — soposadvances bypwandkdecreases by the block’s sum (accounting for what’s now “used up”); otherwise the block does contain the crossing point, sopwis skipped and a finer power of two is tried within the same remaining range. This is bitwise descent (proved in general above) applied with the Fenwick tree’s own binary decomposition as the bit source, giving instead of binary-searching prefix sums with an extra query each (). Non-negative values are required for monotonicity of the prefix sum (the predicate “prefix ” must be a true pattern).
Tested against brute-force linear scan for the smallest prefix , on random non-negative Fenwick arrays.
Other forms
- Exponential (galloping) search. Double
hiuntilpred(hi)holds, then binary search . It costs , which matters when the answer is small but the range is or unbounded. - Integer roots. by binary search with overflow-safe power (stop multiplying once the product exceeds ), or
powfollowed by correction. - Median of two sorted arrays. Binary search the split of the shorter array so that left parts are all right parts: . Generalises to the -th element of two sorted arrays.
Proof: the median-of-two-sorted-arrays search
For a candidate split in array (size ) putting on the left, the matching split in (size , = half the combined length) is forced by needing exactly elements total on the left. The split is valid (the left half is entirely the right half) iff and (boundary comparisons, with sentinels at the edges). As increases by 1, decreases by 1 (to keep the total at ), so is non-decreasing and is non-increasing in — meaning "" flips from true to false at most once as increases (monotone predicate), and symmetrically for the other condition. So the valid split (if it exists, and it always does for a well-formed input) is found by binary searching for the point where both conditions hold — a standard monotone-predicate search, giving by searching the shorter array (searching the longer would cost unnecessarily when ).
- Interpolation search. on uniformly distributed keys, worst case. Rarely useful in contests.
- Binary search over a permutation of candidates. If the predicate is monotone in a sorted order of objects (edges by weight, times of events), sort the objects and binary search the index.
- Interactive binary search. The budget is tight, so every query must halve the candidates exactly (Interactive).
Pitfalls
Verify monotonicity
A non-monotone predicate gives a plausible wrong answer, not a crash. On a small input, brute-force
pred(X)for every and check the pattern isF..FT..T.
- Off-by-one: decide whether you search the first true or the last true, and write it in a comment.
- Overflow inside the check: sums of , products
mid * mid, counts of pairs. Cap them or use__int128. - Real searches with an epsilon loop: use a fixed iteration count.
- The answer may be “impossible”: check
pred(HI)before trusting the result.