Binary search on the answer¶
When you cannot compute the best value directly but can easily test a guess ("is capacity 15 enough?"), and every value above a good guess is also good, binary-search the smallest good guess.
| Time | Extra space | Needs |
|---|---|---|
| \(O(\log R)\) tests, \(R\) = size of the range of possible answers | \(O(1)\) | a monotone test: no, no, ..., no, yes, yes, ..., yes |
The idea¶
A ship must carry packages weighing 1, 2, ..., 10 tonnes, in this order, within 5 days, sailing once a day. What is the smallest ship capacity that makes it?
Computing that capacity directly is awkward. But checking one is easy: load packages in order until the next would overflow, then start a new day, and count the days. And the checks are monotone: if capacity 15 works, 16 works too. So the answers to "does capacity \(c\) work?" look like
and finding the first "yes", \(c^*\), is exactly a binary search, over numbers instead of over a list. We never even build the list of candidates.
How it works¶
- Find a range
[lo, hi]that surely contains the answer: herelo= the heaviest package (it must fit),hi= the total weight (everything in one day). - While
lo < hi:mid = (lo + hi) // 2.- If
test(mid)passes, the answer ismidor smaller:hi = mid. - Otherwise it is bigger:
lo = mid + 1.
lois the smallest value that passes.
Code¶
The second example, the integer square root, searches for the last "yes" (the largest \(r\) with \(r^2 \le n\)),
so it rounds mid up.
def days_needed(weights, capacity):
"""Loads packages in order onto one ship per day; returns how many days that takes."""
days, load = 1, 0
for w in weights:
if load + w > capacity: # this package does not fit today: start a new day
days += 1
load = 0
load += w
return days
def min_capacity(weights, d):
"""The smallest ship capacity that moves every package, in order, within d days."""
lo, hi = max(weights), sum(weights) # lo is too small or just right; hi always works
while lo < hi:
mid = (lo + hi) // 2
if days_needed(weights, mid) <= d: # mid works, so the answer is mid or smaller
hi = mid
else: # mid fails, so the answer is bigger
lo = mid + 1
return lo
def isqrt(n):
"""The largest whole number r with r * r <= n."""
lo, hi = 0, n
while lo < hi:
mid = (lo + hi + 1) // 2 # round up, so lo = mid always makes progress
if mid * mid <= n:
lo = mid
else:
hi = mid - 1
return lo
if __name__ == "__main__":
print(min_capacity([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5))
print(min_capacity([3, 2, 2, 4, 1, 4], 3))
print(min_capacity([1, 2, 3, 1, 1], 4))
print(isqrt(0), isqrt(15), isqrt(16), isqrt(1000000007))
public class ShipCapacity {
/** Loads packages in order onto one ship per day; returns how many days that takes. */
static int daysNeeded(int[] weights, long capacity) {
int days = 1;
long load = 0;
for (int w : weights) {
if (load + w > capacity) { days++; load = 0; } // does not fit today: start a new day
load += w;
}
return days;
}
/** The smallest ship capacity that moves every package, in order, within d days. */
static long minCapacity(int[] weights, int d) {
long lo = 0, hi = 0;
for (int w : weights) { lo = Math.max(lo, w); hi += w; } // lo may be too small; hi always works
while (lo < hi) {
long mid = (lo + hi) / 2;
if (daysNeeded(weights, mid) <= d) hi = mid; // mid works: the answer is mid or smaller
else lo = mid + 1; // mid fails: the answer is bigger
}
return lo;
}
/** The largest whole number r with r * r <= n. */
static long isqrt(long n) {
long lo = 0, hi = Math.min(n, 3037000499L); // 3037000499^2 still fits in a long
while (lo < hi) {
long mid = lo + (hi - lo + 1) / 2; // round up, so lo = mid makes progress
if (mid * mid <= n) lo = mid;
else hi = mid - 1;
}
return lo;
}
public static void main(String[] args) {
System.out.println(minCapacity(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 5));
System.out.println(minCapacity(new int[]{3, 2, 2, 4, 1, 4}, 3));
System.out.println(minCapacity(new int[]{1, 2, 3, 1, 1}, 4));
System.out.println(isqrt(0) + " " + isqrt(15) + " " + isqrt(16) + " " + isqrt(1000000007L));
}
}
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
// Loads packages in order onto one ship per day; returns how many days that takes.
int daysNeeded(const vector<int>& weights, long long capacity) {
int days = 1;
long long load = 0;
for (int w : weights) {
if (load + w > capacity) { days++; load = 0; } // does not fit today: start a new day
load += w;
}
return days;
}
// The smallest ship capacity that moves every package, in order, within d days.
long long minCapacity(const vector<int>& weights, int d) {
long long lo = 0, hi = 0;
for (int w : weights) { lo = max<long long>(lo, w); hi += w; } // lo may be too small; hi works
while (lo < hi) {
long long mid = (lo + hi) / 2;
if (daysNeeded(weights, mid) <= d) hi = mid; // mid works: the answer is mid or smaller
else lo = mid + 1; // mid fails: the answer is bigger
}
return lo;
}
// The largest whole number r with r * r <= n.
long long isqrt(long long n) {
long long lo = 0, hi = min(n, 3037000499LL); // 3037000499^2 still fits in 64 bits
while (lo < hi) {
long long mid = lo + (hi - lo + 1) / 2; // round up, so lo = mid makes progress
if (mid * mid <= n) lo = mid;
else hi = mid - 1;
}
return lo;
}
int main() {
cout << minCapacity({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 5) << "\n";
cout << minCapacity({3, 2, 2, 4, 1, 4}, 3) << "\n";
cout << minCapacity({1, 2, 3, 1, 1}, 4) << "\n";
cout << isqrt(0) << " " << isqrt(15) << " " << isqrt(16) << " " << isqrt(1000000007LL) << "\n";
}
Output: 15, 6, 3, then 0 3 4 31622 for the square roots of 0, 15, 16 and 1,000,000,007.
When to use it¶
Look for the words "minimum maximum" or "maximum minimum", or "the smallest \(x\) such that ...":
- the smallest capacity, speed or time that finishes a job by a deadline,
- splitting an array into \(k\) parts so that the largest part sum is as small as possible,
- placing \(k\) items as far apart as possible (the largest smallest gap),
- the \(k\)-th smallest value in a sorted matrix or among pairwise sums,
- roots of increasing functions, integer square and cube roots.
Common mistakes¶
- A test that is not monotone: the method silently returns a wrong answer. Always ask "if \(x\) works, does every larger \(x\) work?"
- A range that misses the answer:
lomust not exceed it andhimust surely work. - Rounding: searching for the first "yes" uses
midrounded down withhi = mid; searching for the last "yes" needsmidrounded up withlo = mid, or the loop never ends. - Overflow in
mid * midor in sums; use 64-bit integers.
Going deeper¶
Each part below starts closed; open the ones you want.
Sample walkthrough: weights 1..10, 5 days
lo |
hi |
mid |
Days with capacity mid |
Works? |
|---|---|---|---|---|
| 10 | 55 | 32 | 1+..+7 = 28 · 8+9+10 = 27: 2 | yes: hi = 32 |
| 10 | 32 | 21 | 1..6 = 21 · 7+8 = 15 · 9+10 = 19: 3 | yes: hi = 21 |
| 10 | 21 | 15 | 1..5 · 6+7 · 8 · 9 · 10: 5 | yes: hi = 15 |
| 10 | 15 | 12 | 1..4 · 5+6 · 7 · 8 · 9 · 10: 6 | no: lo = 13 |
| 13 | 15 | 14 | 1..4 · 5+6 · 7 · 8 · 9 · 10: 6 | no: lo = 15 |
lo = hi = 15. Six tests instead of trying all 46 capacities from 10 to 55.
The math: why it works and what it costs
Monotone predicate. Let \(P(c)\) be "capacity \(c\) ships everything in at most \(d\) days". If \(c \le c'\), the greedy loading with \(c'\) never starts a new day earlier than with \(c\) (by induction on the packages, the \(c'\)-ship is always at least as far along), so \(\text{days}(c') \le \text{days}(c)\), and \(P(c) \Rightarrow P(c')\).
Greedy loading is optimal for a fixed capacity. Packing each day as full as possible never hurts: any valid plan can be changed, day by day, into the greedy one without using more days. So \(\text{days}(c)\) is the true minimum number of days for capacity \(c\).
Invariant of the search. \(P(hi)\) is true, and \(P(c)\) is false for every \(c < lo\). Each step keeps it and shrinks \(hi - lo\) by about half, so after \(\lceil \log_2 (hi - lo + 1) \rceil\) steps \(lo = hi = c^*\).
Cost. Each test here is \(O(n)\), so the total is
Integer square root. \(P(r) = (r^2 \le n)\) is true then false; we want the last true value, so mid rounds
up. Bounding hi by \(\lfloor\sqrt{2^{63}-1}\rfloor = 3{,}037{,}000{,}499\) keeps mid * mid inside 64 bits.
Watch it step by step
Each guess colours the packages by the day they sail; the info line shows each day's load.
Variations and related ideas
- Real-valued answers: for "the smallest speed to arrive on time", run a fixed number of halvings.
- Parametric search is the general theory: turn an optimization problem into a sequence of decision problems.
- The bisection method for roots of continuous functions is the same idea, guaranteed by the intermediate value theorem.
- Parallel binary search answers many such questions at once by sharing the tests.
See also¶
- Binary search
- Greedy algorithms: the greedy loading used as the test
References¶
- S. Halim, F. Halim, Competitive Programming 3, 2013, Chapter 3 ("binary search the answer").
- N. Megiddo, "Applying parallel computation algorithms in the design of serial algorithms", Journal of the ACM 30(4), 1983 (parametric search).