Appearance
Optimization -> Decision: Binary Search on the Answer
Quick Revisit
- USE WHEN: "minimize/maximize X" where feasibility check is monotone (once achievable, stays achievable)
- INVARIANT: check(T) is monotone — true for all T ≥ T* (min) or T ≤ T* (max)
- TIME: O(log(range) × C) where C = cost of one feasibility check | SPACE: O(1) + check space
- CLASSIC BUG: wrong binary search boundaries or off-by-one — for float answers, use fixed iteration count
- PRACTICE: 08-ladder-mixed
Every optimization problem hides a decision problem. "Find the minimum
Difficulty: [Intermediate]CF Rating Range: 1400-2000 Prerequisites: Binary Search
Contents
- The Meta-Pattern
- When It Works: Monotonicity
- Six Detailed Examples
- When It Fails
- Implementation Notes
- Practice Problems
The Meta-Pattern
The transformation has three parts:
- Original problem: "Minimize
" or "Maximize ." - Decision version: "Is
achievable?" for a threshold . - Binary search on
: run the decision check for each candidate . Narrow the search range until you've found the optimal value.
┌─────────────────────────┐
Optimization │ Binary search over T │ Decision
"minimize X" ──► │ lo=0, hi=MAX │ ──► "is X <= T possible?"
│ while lo < hi: │
│ mid = (lo+hi)/2 │ check(mid)
│ if check(mid): hi=mid │ │
│ else: lo=mid+1 │ true/false
└─────────────────────────┘Total complexity: check(T).
When It Works: Monotonicity
The critical requirement is monotonicity of the predicate:
- For minimization: if
check(T)returns true (achievable for threshold), then check(T')must also return true for all. - For maximization: if
check(T)is true, thencheck(T')is true for all.
In other words, the set of feasible thresholds forms a contiguous suffix (or prefix) of the search space. Binary search finds the boundary.
Why it's usually satisfied: most "minimize the maximum" or "maximize the minimum" problems have this property because relaxing the threshold only makes the constraint easier to satisfy.
Six Detailed Examples
1. Splitting Tasks (in Order) Across Workers
Problem.
Decision version. "Can we split the array into
Check function. Greedy in-order packing: scan once, accumulate into the current block, start a new block whenever adding the next task would exceed
cpp
bool check(long long T, vector<int>& tasks, int k) {
int blocks = 1;
long long cur_load = 0;
for (int d : tasks) {
if (d > T) return false; // single task too large
if (cur_load + d <= T) cur_load += d;
else { blocks++; cur_load = d; }
}
return blocks <= k;
}Binary search range:
Why this is the contiguous version, not the general one. The greedy above is correct only because tasks are split in order (each worker gets a prefix-block). The harder problem — "
identical machines, each task can go on any machine, minimize makespan" — is bin-packing-flavored and NP-hard. There is no polynomial greedy check for it; you would need PTAS / LPT heuristics or DP for small . If a problem allows arbitrary task-to-machine assignment, the binary-search-plus-greedy template here is wrong.
2. Maximum Minimum Distance (Aggressive Cows)
Problem:
Decision version: "Can we place
Check function (greedy placement): place the first cow at
cpp
bool check(int D, vector<int>& x, int k) {
int placed = 1, last = x[0];
for (int i = 1; i < (int)x.size(); i++) {
if (x[i] - last >= D) { placed++; last = x[i]; }
}
return placed >= k;
}Search range:
3. Minimum Maximum Pages Per Worker
Problem:
Decision version: "Can
Check function (greedy assignment): scan left to right, accumulating pages. When the running sum exceeds
cpp
bool check(long long P, vector<int>& pages, int k) {
int workers = 1;
long long cur = 0;
for (int p : pages) {
if (p > P) return false; // single book exceeds limit
if (cur + p > P) { workers++; cur = p; }
else cur += p;
}
return workers <= k;
}Classic "painter's partition" / "split array largest sum" problem.
4. K-th Smallest Element in a Sorted Matrix
Problem:
Decision version: "How many elements
Check function: for each row, binary search (or use the sorted-column property to do a staircase walk) to count elements
cpp
int countLeq(vector<vector<int>>& mat, int X) {
int n = mat.size(), cnt = 0;
int col = n - 1;
for (int row = 0; row < n; row++) {
while (col >= 0 && mat[row][col] > X) col--;
cnt += col + 1;
}
return cnt;
}Binary search range:
5. Minimum Cost to Cut Array into K Pieces (DP Check)
Problem: cut an array into exactly
Decision version: "Can we partition into
For simple contiguous partitions, a greedy check works (same as example 3). But in harder variants (e.g., with additional constraints on piece structure), the check may need a DP that runs in
The outer binary search contributes only a
6. Parallel Binary Search: Many Queries Offline
Problem:
Idea: maintain an interval
This technique is essential for problems like "for each element, find the earliest time it satisfies condition
When It Fails
Binary search on the answer requires monotonicity. Watch out for:
- Non-monotonic predicates. "Can we partition into
pieces with each piece sum exactly equal to ?" Being achievable for doesn't imply achievable for . Binary search doesn't apply. - Discrete answer spaces with gaps. If the answer can only be certain scattered values (e.g., specific primes), binary search on a continuous range may land between valid answers. You may need to search over the discrete candidates directly.
- Floating-point answers. Binary search still works, but you must be careful with precision. Use a fixed number of iterations (e.g., 100) rather than
while (hi - lo > eps), which can loop forever for very smalleps.
Implementation Notes
This topic is as much about avoiding off-by-one bugs as it is about recognizing monotonicity. Memorize one template per direction and never deviate.
Integer template: minimize subject to check(T)
Invariant. Throughout the loop, the answer lies in check(\text{hi}) is always true; check(\text{lo} - 1) is always false (or lo starts at the global minimum).
cpp
long long lo = LO_MIN, hi = HI_MAX; // hi must satisfy check(hi)
while (lo < hi) {
long long mid = lo + (hi - lo) / 2; // round down
if (check(mid)) hi = mid; // shrink upper bound
else lo = mid + 1; // mid impossible, exclude it
}
// lo == hi == smallest T with check(T) == trueThree things to verify before trusting the answer:
- The initial
hiis feasible — i.e.,check(hi)returns true. If not, the loop returns garbage. Always sethito a safe upper bound (sum of all elements, large enough constant, etc.). mid = lo + (hi - lo) / 2rather than(lo + hi) / 2to avoid overflow on near-LLONG_MAXranges.- The loop condition is
lo < hi(strict). If you writelo <= hi, you'll need a different update pattern and a separateansvariable.
Integer template: maximize subject to check(T)
Mirror image: lo is always feasible, hi is the smallest infeasible.
cpp
long long lo = LO_MIN, hi = HI_MAX; // lo must satisfy check(lo)
while (lo < hi) {
long long mid = lo + (hi - lo + 1) / 2; // round UP
if (check(mid)) lo = mid; // mid feasible, push up
else hi = mid - 1; // mid infeasible
}
// lo == hi == largest T with check(T) == trueThe rounding direction is the only difference from the minimization template. Round up so mid > lo whenever lo + 1 < hi; otherwise the loop never advances.
Floating-point template
cpp
double lo = LO_MIN, hi = HI_MAX;
for (int iter = 0; iter < 200; iter++) { // 200 iters >> log2(1e18)
double mid = (lo + hi) / 2;
if (check(mid)) hi = mid; // (or lo = mid for max)
else lo = mid;
}
double ans = lo; // lo and hi are within 2^-200Use a fixed iteration count rather than while (hi - lo > eps) — floating-point underflow can make the loop run forever. 100 iterations already gets you below
Practice Problems
| Problem | Pattern | Source |
|---|---|---|
| CF 460C -- Present | Binary search + greedy range check | CF |
| CF 551C -- GukiZ hates Boxes | Binary search on time + greedy simulation | CF |
| CF 1223D -- Equalizing by Division | Binary search on answer + multiset check | CF |
| SPOJ AGGRCOW | Maximum minimum distance (classic) | SPOJ |
| LC 410 -- Split Array Largest Sum | Painter's partition | LeetCode |