Binary search: on a range, and on the answer
Halving a sorted range, and the larger trick of halving the space of possible answers.
By the end of this chapter you can
- Use lower_bound, upper_bound and equal_range correctly
- Write a binary search whose invariant you can state
- Recognise a problem where the answer itself can be searched for
Binary search is two techniques wearing one name. The first is finding a value in a sorted array, which the standard library already does. The second is finding the boundary of a monotone predicate — and that one turns a large class of “what is the smallest X such that…” problems into twenty iterations of a loop.
The second is what makes this chapter worth its length. The first is what you have to get exactly right before it works.
The library’s four
Every one of these needs a range that is sorted with respect to the same comparison you are searching with. On an unsorted range they compile, run, and return nonsense.
#include <algorithm>
#include <cstdio>
#include <vector>
int main() {
std::vector<int> v{1, 3, 3, 3, 5, 8};
// Is it there at all?
std::printf("binary_search(3): %d binary_search(4): %d\n",
std::binary_search(v.begin(), v.end(), 3),
std::binary_search(v.begin(), v.end(), 4));
// First element not less than x -> how many are strictly smaller.
auto low = std::lower_bound(v.begin(), v.end(), 3);
// First element greater than x -> how many are less than or equal.
auto high = std::upper_bound(v.begin(), v.end(), 3);
std::printf("lower_bound(3) at index %ld, upper_bound(3) at index %ld\n",
low - v.begin(), high - v.begin());
std::printf("count of 3s: %ld\n", high - low);
// Both at once.
auto [first, last] = std::equal_range(v.begin(), v.end(), 3);
std::printf("equal_range(3): [%ld, %ld)\n", first - v.begin(), last - v.begin());
// A value that is absent: both bounds land at the insertion point.
auto missing = std::lower_bound(v.begin(), v.end(), 4);
std::printf("lower_bound(4) at index %ld, and *it == %d\n",
missing - v.begin(), *missing);
}The distinction that matters:
| Returns | |
|---|---|
lower_bound(x) |
first element not less than x — so >= x |
upper_bound(x) |
first element greater than x |
equal_range(x) |
both, as a pair |
binary_search(x) |
just whether x is present |
lower_bound is the one you want almost always: its index is the count of
elements strictly smaller, it is where x would be inserted to keep the range
sorted, and it points at x itself when x is present.
Writing one, with an invariant
The library’s version is enough for a sorted array. For everything else you write the loop, and the way to write it correctly is to state the invariant first.
The invariant: the answer is always in [lo, hi]. Every iteration shrinks
that interval without ever excluding the answer, and the loop ends when it holds
exactly one value.
#include <cstdio>
#include <vector>
// The smallest index i such that v[i] >= target, or v.size() if none.
std::size_t first_at_least(const std::vector<int>& v, int target) {
std::size_t lo = 0;
std::size_t hi = v.size(); // one past the last candidate
while (lo < hi) { // invariant: the answer is in [lo, hi]
std::size_t mid = lo + (hi - lo) / 2;
if (v[mid] >= target) hi = mid; // mid is a candidate; keep it
else lo = mid + 1; // mid is not; discard it
}
return lo; // lo == hi: one candidate left
}
int main() {
std::vector<int> v{1, 3, 3, 3, 5, 8};
for (int target : {0, 1, 3, 4, 8, 9})
std::printf("first index with value >= %d: %zu\n", target, first_at_least(v, target));
}Four rules make this loop correct, and breaking any one of them causes an infinite loop or an off-by-one:
histarts one past the last candidate, so[lo, hi)is the search space andhiis a legitimate answer meaning “none of them”.while (lo < hi), not<=. Withhiexclusive,lo == himeans the space is empty and the answer islo.hi = midwhenmidis a candidate — nevermid - 1, which would discard a possible answer.lo = mid + 1when it is not — neverlo = mid, which does not shrink the interval whenloandhiare adjacent, and hangs.
Write it this way every time and the off-by-one questions stop arising. The
version that asks “should this be mid or mid ± 1” case by case is the
version that has a bug in it somewhere.
The midpoint overflows
#include <cstdio>
int main() {
int lo = 2'000'000'000;
int hi = 2'000'000'001;
int mid = (lo + hi) / 2; // 4 * 10^9 does not fit in an int
std::printf("mid = %d\n", mid);
}runtime error: signed integer overflow: 2000000000 + 2000000001 cannot be represented in type ‘int’
It prints -147483647 — a midpoint outside the range, which indexes out of
bounds or loops forever. Josh Bloch found this in java.util.Arrays.binarySearch
in 2006, where it had been since 1997 and had been copied out of a textbook.
lo + (hi - lo) / 2 cannot overflow when lo <= hi, because hi - lo is at
most the range size. Write it that way always; it costs nothing and removes a
whole category of bug.
This matters far more in the next section than it does when searching an array,
because there hi is a value rather than an index and can be 10¹⁸.
Binary search on the answer
Here is the technique that earns the chapter.
Many problems ask for the smallest (or largest) value satisfying some condition, where the condition is monotone: if a value works, everything larger works too. When that holds, you do not need to compute the answer. You need only be able to check a candidate, and then you can search for the boundary.
The recipe:
- Identify the candidate answers and check that the predicate is monotone:
false, false, …, false, true, true, …, true. - Write
feasible(x)— can it be done withx? - Binary search for the first
xwherefeasible(x)is true.
The cost is log(range) calls to feasible, which for a range of 10¹⁸ is sixty.
#include <cstdio>
#include <vector>
// Packages must ship in their given order, at most `capacity` per day.
// Can they all go within `days` days?
bool feasible(const std::vector<int>& weights, int days, long long capacity) {
long long used = 1;
long long load = 0;
for (int w : weights) {
if (w > capacity) return false; // one package alone will not fit
if (load + w > capacity) { ++used; load = 0; }
load += w;
}
return used <= days;
}
long long min_capacity(const std::vector<int>& weights, int days) {
long long lo = 1; // any capacity below 1 is useless
long long hi = 0;
for (int w : weights) hi += w; // one day for everything always works
while (lo < hi) {
long long mid = lo + (hi - lo) / 2;
if (feasible(weights, days, mid)) hi = mid; // mid works; look lower
else lo = mid + 1;
}
return lo;
}
int main() {
std::vector<int> weights{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for (int days : {1, 2, 3, 5, 10})
std::printf("%2d days -> capacity %lld\n", days, min_capacity(weights, days));
}Ten days needs a capacity of 10 — the largest single package. One day needs 55,
the total. The interesting values are in between, and no formula produces them:
feasible is a greedy simulation, and the search finds the boundary between
“too small” and “enough”.
Notice what made this work. Nothing about the problem is sorted. What is monotone is the predicate: if capacity 20 is enough, so is 21. That is the only property the technique needs.
How to spot one. The signals are: the question says “minimum X such that” or “maximum X such that”; checking a candidate is much easier than constructing the optimum; and the answer’s range is large but bounded. Chapter 10.1’s constraint table points here too — an answer bounded by 10¹⁸ with a one-second limit leaves room for about sixty checks and nothing else.
Searching a real-valued answer
When the answer is a double, halving cannot terminate exactly. Fix the
iteration count instead:
double lo = 0, hi = 1e9;
for (int step = 0; step < 100; ++step) { // 100 halvings, not a condition
double mid = lo + (hi - lo) / 2;
if (feasible(mid)) hi = mid; else lo = mid;
}
// lo and hi now agree to far more precision than any judge asks forA hundred iterations divides the interval by 2¹⁰⁰, which is beyond double’s
precision long before it finishes — so a hundred is a safe number to write
without thinking. A while (hi - lo > 1e-9) condition is the version that hangs
when the interval cannot get that small in floating point.