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 arrays — Understanding standard binary search mechanics helps transition to searching over an implicit answer space.
- →Greedy algorithms — Many feasibility checks rely on greedy placement or accumulation strategies.
- →Time and space complexity — You must estimate check_cost and ensure overall efficiency.
- →Sorting and two-pointer scans — Several checks require sorted inputs and linear scans to maintain feasibility.
- →Integer arithmetic and overflow — Binary search bounds and mid calculations must be overflow-safe using 64-bit types.
- →Floating-point precision — Real-valued searches need epsilon-based stopping and careful rounding when converting to integers.
Detailed Explanation
Tap terms for definitions01Overview
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
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
Code Examples
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Check if we can place k cows with minimum pairwise distance >= d 5 bool 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 18 int 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.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Check if we can split array into <= m parts with each part sum <= limit 5 bool 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 21 int 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.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Given machine times t[i], can we produce at least target items within time T? 5 bool 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 14 int 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.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Check if we can cut at least k pieces of length L from the ropes 5 bool 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 15 int 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.