⚙️AlgorithmIntermediate

Binary Search on Answer

Key Points

  • Binary Search on Answer turns an optimization problem into a yes/no decision problem over a monotonic answer space.
  • You define a predicate P(x) that checks whether a candidate answer x is feasible, and then binary search the smallest or largest x that satisfies your goal.
  • This approach works when feasibility is monotonic: if x works, then all larger (or smaller) values also work.
  • The total time is O(log(range) × chec), where range is the size of the answer domain and chec is the complexity of P(x).
  • Use greedy or simulation in P(x) to verify feasibility efficiently.
  • Be careful with off-by-one errors, choosing mid, and integer overflow when computing mid and bounds.
  • Use the lower-mid pattern to minimize a value and upper-mid to maximize a value in integer searches.
  • For real-valued answers, run a fixed number of iterations or stop when the interval length is below a chosen epsilon.

Prerequisites

  • Basic binary search on arraysUnderstanding standard binary search mechanics helps transition to searching over an implicit answer space.
  • Greedy algorithmsMany feasibility checks rely on greedy placement or accumulation strategies.
  • Time and space complexityYou must estimate check_cost and ensure overall efficiency.
  • Sorting and two-pointer scansSeveral checks require sorted inputs and linear scans to maintain feasibility.
  • Integer arithmetic and overflowBinary search bounds and mid calculations must be overflow-safe using 64-bit types.
  • Floating-point precisionReal-valued searches need epsilon-based stopping and careful rounding when converting to integers.

Detailed Explanation

Tap terms for definitions

01Overview

Binary Search on Answer is a powerful technique for solving optimization problems by transforming them into a sequence of decision problems. Instead of directly computing the optimal value, you ask: given a candidate answer x, can I achieve it? If the feasibility of x is monotonic—meaning that if x is feasible then all larger (or smaller) values are also feasible—then you can apply binary search over the space of possible answers. At each step, you evaluate a predicate P(x) that returns true if x is achievable and false otherwise. Based on the result, you shrink the search interval until you isolate the optimal value. This method is especially useful when the feasibility check can be done efficiently with a greedy algorithm or a linear scan, while a direct computation of the optimum is hard. Classic problems include minimizing the maximum load, maximizing the minimum distance, finding the minimum time to produce items with machines, and cutting ropes to maximize equal piece length. The key advantage is predictable performance: O(log(range) × check_cost), where range is the width of the answer domain (e.g., max−min) and check_cost is the cost of evaluating P(x).

02Intuition & Analogies

Imagine you are setting the volume on a speaker for a classroom. You want it just loud enough so students at the back can hear clearly. If a certain volume V works, then any louder volume also works. Instead of trying every possible volume, you can jump around: try the middle volume; if it works, try lower; if not, try higher. This is binary search on the answer. Another analogy: suppose you want to ship packages in D days and you are choosing a truck capacity C. If a capacity C can complete the job in D days (by simulating day-by-day loading), then any capacity larger than C can also work. That monotonicity lets you search capacities with binary search: test a mid capacity, simulate, and keep the half of the search that could contain the answer. Or think of placing Wi‑Fi routers in a hallway: if you can place them so that the minimum distance between routers is at least d, then you can certainly place them with a smaller required distance d′ < d. Thus feasibility is monotone decreasing with respect to the required distance, which means you can search for the largest d that still works by checking mid distances greedily. For continuous answers (like cutting ropes to the longest equal length), the same idea applies but you stop once your interval is small enough (epsilon), just as you might zoom in on a map until you’re within a street’s width of your destination. The essence is: define a yes/no check that gets easier as you move in one direction, and then use binary search to locate the boundary between 'no' and 'yes.'

03Formal Definition

Let D be a totally ordered domain of candidate answers (e.g., integers or reals), and let P: D → {true, false} be a predicate such that P is monotonic. For minimization with a lower threshold, we assume P is non-decreasing: for all x, y ∈ D with , P(x) ⇒ P(y). In this setting, we seek the minimal x* ∈ D such that P(x*) = true. For maximization with an upper threshold, we assume P is non-increasing: for all , P(y) ⇒ P(x). We then seek the maximal x* such that P(x*) = true. Binary Search on Answer maintains an interval [lo, hi] ⊆ D that is guaranteed to contain the optimum x*. At each step, compute )/2) for integers (or mid = (lo + hi)/2 for reals), evaluate P(mid), and narrow the interval according to monotonicity: if minimizing and P(mid) is true, set else set . If maximizing and P(mid) is true, set else set − 1. The algorithm terminates when (discrete case) or when hi − lo ≤ ε (continuous case), yielding x*. The total complexity is O(lo() × C), where C is the cost of evaluating P.

04When to Use

Use Binary Search on Answer when: (1) You can define a feasible(x) predicate that can be checked in polynomial or linear time. Examples: given a capacity C, can we pack items over D days? Given a distance d, can we place k routers? (2) The feasible set is monotonic with respect to x—either everything above a threshold works or everything below a threshold works. (3) The direct closed-form solution is unclear or complex, but simulation/greedy is straightforward. Typical problem families include: minimizing the maximum (e.g., splitting an array into m parts minimizing the largest sum), maximizing the minimum (e.g., placing k cows to maximize minimum distance), time-to-complete (e.g., minimum time for machines to produce n items), and cutting/partitioning (e.g., rope cutting to get at least k pieces). Consider it especially when the answer range is large (e.g., up to 10^9) yet the feasibility check is O(n) or O(n log n). Avoid it if feasibility is non-monotonic, the check requires exponential time, or the answer domain is extremely small (simple brute force may suffice). For floating-point answers, ensure your application tolerates epsilon error and prefers approximate numerical solutions.

⚠️Common Mistakes

Common pitfalls include: (1) Breaking monotonicity: writing a check function that doesn’t respect the needed monotone property, often due to bugs in a greedy/simulation step. Always reason about why P(x) being true implies feasibility for all larger/smaller x as required. (2) Wrong mid and infinite loops: for integer maximization you often need the upper-mid mid = (lo + hi + 1)/2; otherwise lo may never advance when P(mid) is true. For minimization use lower-mid mid = (lo + hi)/2. (3) Overflow computing mid: use mid = lo + (hi − lo)/2 with 64-bit integers to avoid lo + hi overflow. (4) Incorrect bounds: choose lo and hi so that P(lo) and P(hi) straddle the boundary (e.g., for minimization, P(lo) = false and P(hi) = true). If hi is too small or lo too large, the answer may be excluded. (5) State leakage: the check function should be pure with respect to the search—do not mutate shared state that affects later checks unless you reset it. (6) Precision issues: for real-valued searches, stop with a fixed iteration count (e.g., 60–100) or until hi − lo ≤ ε, and be consistent with rounding when converting to integers. (7) Ignoring corner cases: arrays of size 1, k = 1, all elements equal, or answers at boundaries (0 or max range). (8) Complexity blow-up in check: avoid O(n^2) checks inside the loop; aim for O(n) or O(n log n) to keep the overall algorithm efficient.

Key Formulas

Iterations for Integer Binary Search

Explanation: The number of iterations needed to shrink an integer interval [lo, hi] to a single point is logarithmic in its size. This bounds how many times we evaluate the feasibility predicate.

Time Complexity

Explanation: Overall time equals logarithmic iterations over the answer range R times the cost C(n) of the check function. If C(n) is linear, the algorithm is near-linear up to a log factor.

Overflow-safe Midpoint

Explanation: Computing the midpoint this way avoids potential overflow from lo + hi when using fixed-width integers. It is the standard safe pattern in binary search.

Monotonic Predicate

Explanation: The predicate P must switch only once across the domain D. For minimization, it is non-decreasing; for maximization, it is non-increasing. This property ensures binary search correctness.

Minimization Target

Explanation: In minimization, we look for the smallest x that satisfies the predicate. Binary search guarantees we find this threshold if P is non-decreasing and bounds are chosen correctly.

Maximization Target

Explanation: In maximization, we look for the largest feasible x. With a non-increasing P and correct bounds, binary search converges to this boundary.

Convergence for Real Binary Search

Explanation: Each iteration halves the interval, so the remaining error shrinks exponentially. Choose t so that the residual error is below your epsilon tolerance.

Split Feasibility Bound (intuition)

Explanation: While not a strict equivalence in all cases, this inequality guides the check function for splitting arrays: simulate forming parts whose sums stay under S to verify feasibility.

Complexity Analysis

Binary Search on Answer performs a logarithmic number of iterations over the answer range R. For integer domains, the number of steps is at most ceil(log2(R)), where − lo + 1. Each step invokes the feasibility predicate P(x), whose cost typically dominates. If P(x) runs in linear time O(n), the total complexity becomes O(n log R). For example, in maximizing the minimum distance between points after sorting, the check runs in O(n), and sorting (if needed once) adds O(n log n) preprocessing; the search then adds O(n log R). In problems like splitting an array into m parts minimizing the largest sum, the check also runs in O(n), leading to O(n log R) total time, where R ranges from max() to sum(). In time-to-complete problems (e.g., machines producing items), the check cost is O(k) for k machines, thus O(k log R), with R up to mi × target. For continuous searches using doubles, running t iterations yields an error at most (hi − lo)/2^t; typically t ≈ 60 suffices for 64-bit doubles. Space usage is modest: the search itself is O(1) extra space beyond the input. Some checks may need additional O(1) to O(n) working memory—for instance, a greedy linear pass with counters. Preprocessing like sorting uses O(1) auxiliary space if in-place (e.g., std::sort) or O(n) otherwise. Overall, Binary Search on Answer is attractive because it trades an exponential or combinatorial direct search for a predictable logarithmic factor times a simple linear-time feasibility test.

Code Examples

Maximize Minimum Distance (Aggressive Cows / Router Placement)
1#include <bits/stdc++.h>
2using namespace std;
3
4// Check if we can place k cows with minimum pairwise distance >= d
5bool canPlace(const vector<int>& pos, int k, int d) {
6 int count = 1; // place first cow at pos[0]
7 int last = pos[0];
8 for (size_t i = 1; i < pos.size(); ++i) {
9 if (pos[i] - last >= d) { // greedy: place at earliest valid position
10 ++count;
11 last = pos[i];
12 if (count >= k) return true; // early exit if all placed
13 }
14 }
15 return false;
16}
17
18int main() {
19 ios::sync_with_stdio(false);
20 cin.tie(nullptr);
21
22 vector<int> positions = {1, 2, 4, 8, 9};
23 int k = 3; // number of cows (routers)
24
25 sort(positions.begin(), positions.end());
26
27 int lo = 0; // smallest possible distance
28 int hi = positions.back() - positions.front(); // largest possible distance
29
30 // We maximize the minimum distance, so use upper-mid to avoid infinite loops
31 while (lo < hi) {
32 int mid = lo + (hi - lo + 1) / 2; // upper-mid
33 if (canPlace(positions, k, mid)) {
34 lo = mid; // feasible: try a larger distance
35 } else {
36 hi = mid - 1; // infeasible: shrink high
37 }
38 }
39
40 cout << "Max minimal distance = " << lo << "\n"; // expected 3 for this data
41 return 0;
42}
43

We binary search the answer d, the minimum distance between placed cows. The predicate canPlace greedily places cows from left to right at the earliest stall maintaining distance ≥ d. Since feasibility is monotone in d (if distance d works, any smaller distance also works), we use upper-mid and move lo up on success to maximize d.

Time: O(n log R) after sorting (R = max_gap), overall O(n log n + n log R)Space: O(1) extra (in-place sorting aside)
Minimize the Largest Subarray Sum (Split Array into m Parts)
1#include <bits/stdc++.h>
2using namespace std;
3
4// Check if we can split array into <= m parts with each part sum <= limit
5bool canSplit(const vector<int>& a, int m, long long limit) {
6 long long current = 0;
7 int parts = 1;
8 for (int x : a) {
9 if (x > limit) return false; // single element exceeds limit => impossible
10 if (current + x <= limit) {
11 current += x;
12 } else {
13 ++parts;
14 current = x;
15 if (parts > m) return false; // too many parts
16 }
17 }
18 return true;
19}
20
21int main() {
22 ios::sync_with_stdio(false);
23 cin.tie(nullptr);
24
25 vector<int> a = {7, 2, 5, 10, 8};
26 int m = 2;
27
28 long long lo = *max_element(a.begin(), a.end()); // lower bound on answer
29 long long hi = accumulate(a.begin(), a.end(), 0LL); // upper bound on answer
30
31 // We minimize the maximum part sum, so use lower-mid and move hi down on success
32 while (lo < hi) {
33 long long mid = lo + (hi - lo) / 2; // lower-mid
34 if (canSplit(a, m, mid)) {
35 hi = mid; // feasible: try a smaller maximum sum
36 } else {
37 lo = mid + 1; // infeasible: need larger maximum sum
38 }
39 }
40
41 cout << "Minimized largest sum = " << lo << "\n"; // expected 18 for this data
42 return 0;
43}
44

We search over the candidate maximum subarray sum S. The feasibility check greedily forms parts without exceeding S; if we need more than m parts, S is too small. Since larger S never hurts feasibility, the predicate is monotone non-decreasing, allowing a standard minimization binary search.

Time: O(n log R), where R = sum(a) − max(a) + 1Space: O(1) extra
Minimum Time for Machines to Produce Items (Factory Machines)
1#include <bits/stdc++.h>
2using namespace std;
3
4// Given machine times t[i], can we produce at least target items within time T?
5bool canProduce(const vector<long long>& t, long long target, long long T) {
6 __int128 total = 0; // use wider accumulator to prevent overflow
7 for (long long ti : t) {
8 total += T / ti; // items produced by machine i
9 if (total >= target) return true; // early stop to save time
10 }
11 return total >= target;
12}
13
14int main() {
15 ios::sync_with_stdio(false);
16 cin.tie(nullptr);
17
18 vector<long long> machines = {2, 3, 7};
19 long long target = 15;
20
21 long long lo = 0;
22 long long fastest = *min_element(machines.begin(), machines.end());
23 long long hi = fastest * target; // safe upper bound: fastest machine does all
24
25 while (lo < hi) {
26 long long mid = lo + (hi - lo) / 2;
27 if (canProduce(machines, target, mid)) {
28 hi = mid; // feasible: can we do it faster?
29 } else {
30 lo = mid + 1; // infeasible: need more time
31 }
32 }
33
34 cout << "Minimum time = " << lo << "\n"; // prints minimal time to make 'target' items
35 return 0;
36}
37

We search the minimal time T to produce a target number of items. The predicate sums floor(T / t[i]) across machines; if the total is at least target, T is sufficient. Feasibility is monotone in T, so standard minimization binary search applies. Using a wide accumulator prevents overflow.

Time: O(k log R), where k is number of machines and R ≈ fastest * targetSpace: O(1) extra
Maximize Rope Piece Length (Real-Valued Binary Search)
1#include <bits/stdc++.h>
2using namespace std;
3
4// Check if we can cut at least k pieces of length L from the ropes
5bool canCut(const vector<double>& ropes, int k, double L) {
6 if (L <= 0) return true; // trivially true, but we avoid L=0 in search
7 long long count = 0;
8 for (double r : ropes) {
9 count += static_cast<long long>(r / L); // floor(r / L)
10 if (count >= k) return true; // early exit
11 }
12 return false;
13}
14
15int main() {
16 ios::sync_with_stdio(false);
17 cin.tie(nullptr);
18
19 vector<double> ropes = {8.02, 7.43, 4.57, 5.39};
20 int k = 11;
21
22 double lo = 0.0;
23 double hi = *max_element(ropes.begin(), ropes.end());
24
25 // Perform fixed iterations to control precision (epsilon ≈ (hi-lo)/2^iters)
26 for (int it = 0; it < 70; ++it) { // ~1e-21 relative shrink; plenty for printing 2-6 decimals
27 double mid = 0.5 * (lo + hi);
28 if (canCut(ropes, k, mid)) {
29 lo = mid; // feasible: try longer pieces
30 } else {
31 hi = mid; // infeasible: shorten pieces
32 }
33 }
34
35 cout.setf(std::ios::fixed); cout << setprecision(2);
36 cout << "Max piece length (≈) = " << lo << "\n"; // print with 2 decimals
37 return 0;
38}
39

We maximize the piece length L so that at least k pieces can be cut. The predicate counts floor(r/L) across all ropes, which is monotone decreasing in L. For real-valued answers, we iterate a fixed number of times or until the interval is below epsilon and then print with desired precision.

Time: O(n × I), where I is the number of iterations (e.g., 60–100)Space: O(1) extra