DSA DSA In Depth
0%
design parametric-searchbinary-searchoptimization

Parametric Search: Binary Searching the Answer Space & Feasibility Predicates

7 min read Module 2.3: Binary Search on Monotonic Spaces & Divide-and-Conquer Updated Oct 23, 2026
📄 Cheat Sheet
Parametric Search: Binary Searching the Answer Space with Monotonic Predicates
Luv CP Parametric Search: Binary Searching the Answer Space with Monotonic Predicates Watch on YouTube ↗

Shows how optimization problems ('minimize the maximum capacity/time X') reduce to binary searching an implicit monotonic decision predicate feasible(X).

0:00 / --:--

Optimization vs. Decision Duality (Megiddo's Parametric Search)

Many combinatorial optimization problems have the form:

$$\min \,\{ X \in [L, R] : \text{feasible}(X) = \text{True} \} \quad \text{or} \quad \max \,\{ X \in [L, R] : \text{feasible}(X) = \text{True} \}$$

Constructing the globally optimal schedule or partition from scratch can require dynamic programming in $O(N^2 K)$ time. Yet checking the decision problem—given a candidate threshold $X$, does there exist a valid configuration with cost $\le X$?—can often be solved by a simple $O(N)$ greedy scan! This reduction is valid whenever the feasibility predicate satisfies monotonicity:

  • Minimization (False $\to$ True): If budget $X$ is sufficient, any larger budget $X' > X$ is also sufficient ($[F, F, \dots, F, T, T, \dots, T]$). We seek the first True.
  • Maximization (True $\to$ False): If minimum separation $X$ is achievable, any smaller separation $X' < X$ is also achievable ($[T, T, \dots, T, F, F, \dots, F]$). We seek the last True.
Three Classic Parametric Search Reductions
  1. Min-Max Array Partitioning (Painter's Partition / Split Array Largest Sum / Book Allocation): Partition an array of non-negative weights $w_0, \dots, w_{n-1}$ into at most $k$ contiguous subarrays to minimize the maximum subarray sum $X$.
    Bounds: $L = \max_i w_i$ (each item must fit in some bucket), $R = \sum_i w_i$ (all items in 1 bucket).
    Greedy Check $O(n)$: Pack items left-to-right into the current bucket as long as cur_sum + w[i] <= X; start a new bucket whenever it overflows. $X$ is feasible iff total buckets used $\le k$.
  2. Max-Min Geometric Placement (Aggressive Cows / Magnetic Force): Place $k$ points among $n$ sorted coordinates $x_0 < \dots < x_{n-1}$ to maximize the minimum distance $D$ between any pair.
    Greedy Check $O(n)$: Place the first point at $x_0$, and greedily place each subsequent point at the first $x_i \ge \text{last\_pos} + D$. $D$ is feasible iff $\ge k$ points are placed.
  3. 0-1 Fractional Programming (Maximum Ratio Cycle / Best Average Subset): Maximize $\frac{\sum_{i \in S} a_i}{\sum_{i \in S} b_i}$ ($b_i > 0$) subject to $|S| \ge k$.
    Dinkelbach / Parametric Reduction: A ratio $\ge \lambda$ is achievable iff $\max_{|S| \ge k} \sum_{i \in S} (a_i - \lambda b_i) \ge 0$! Transforming each item's weight to $c_i(\lambda) = a_i - \lambda b_i$ turns a non-linear ratio optimization into a linear sum check!
Problem Class Direct DP / Naive Complexity Parametric Search Complexity Monotonicity Prerequisite
Split Array into $K$ Subarrays $O(N^2 K)$ DP $O\!\left(N \log(\sum w_i)\right)$ $w_i \ge 0$ mandatory! (Negative weights break greedy packing)
$K$-th Smallest in $M \times N$ Sorted Matrix $O(M N \log(MN))$ or $O(K \log M)$ $O\!\left((M + N) \log(\text{max} - \text{min})\right)$ Count of elements $\le X$ is monotonically non-decreasing in $X$
0-1 Fractional Programming ($\max \frac{\sum a_i}{\sum b_i}$) Exponential / Non-linear $O\!\left(N \log N \cdot \log(\frac{\text{range}}{\varepsilon})\right)$ $b_i > 0$ so $f(\lambda) = \max \sum(a_i - \lambda b_i)$ is strictly decreasing
Continuous Real-Valued Search Analytical solver Fixed $60\text{–}80$ iterations ($2^{-60} \approx 10^{-18}$) Monotonic continuous predicate over IEEE-754 double
Parametric Search Pipeline: Decoupling the Binary Search Driver from the O(N) Greedy Verifier Answer Space [lo, hi] lo = max(w_i), hi = sum(w_i) Propose mid = lo + (hi-lo)/2 Candidate X O(N) Greedy Oracle feasible(X): Count buckets needed Returns: buckets_used <= K True → hi = mid False → lo = mid+1 Even if sum(w_i) = 10^14, log₂(10^14) ≈ 47 iterations! The O(N) greedy check runs only 47 times over L1-cached data.
flowchart LR A["Optimization Goal: Minimize Max Cost X"] --> B["Identify Tight Bounds: lo = max_i(w_i), hi = sum_i(w_i)"] B --> C["Write O(N) Decision Oracle: bool feasible(X)"] C --> D["Verify Monotonicity: Does feasible(X) ⇒ feasible(X+1)?"] D --> E["Half-Open Binary Search on [lo, hi) in O(N log(hi - lo))"]
Half-Open [lo, hi) Monotonic Predicate Decision Boundary Search
O(log N) · Zero Off-By-One

Evaluate predicate P(x) over a monotonic boolean space [F, F, ..., F, T, T, ..., T]. Maintain the loop invariant P(lo−1) == False and P(hi) == True until lo == hi.

Step 1 / 11
Custom Input:
Timeline

Invariant & State

Initial Sorted Monotonic Domain: [0, 8) | Threshold P(x): x ≥ 47 (Boundary index = 5)
📊 Live Magnitude & Pointer Stage — Half-Open [lo, hi) Monotonic Predicate Decision Boundary Search Active Pivot Match/Sorted
P=False 10 [0] P=False 18 [1] P=False 24 [2] P=False 31 [3] P=False 39 [4] P=True 47 [5] P=True 55 [6] P=True 68 [7]
[0]10P=False
[1]18P=False
[2]24P=False
[3]31P=False
[4]39P=False
[5]47P=True
[6]55P=True
[7]68P=True
Interval [lo, hi)
[0, 8)
Predicate P(x)
x ≥ 47
Monotonic Split
F×5 then T×3
Max Probes ⌈log₂N⌉
4
[Step 1] Initialized half-open interval [lo=0, hi=8) over sorted array [10, 18, 24, 31, 39, 47, 55, 68] for predicate P(x): x ≥ 47.
Hardware Reality: Floating-Point Binary Search Trap (while (hi - lo > eps) Infinite Loops!)

When binary searching over continuous IEEE-754 double values, writing while (hi - lo > 1e-9) is a classic production time-bomb! An IEEE-754 double has 53 bits of significand precision ($approx 15 ext{–}17$ decimal digits). If your answer $X$ is around $10^8$, the spacing between adjacent representable doubles (1 ULP = $2^{-52} \times 10^8 \approx 2.2 \times 10^{-8}$) is larger than 1e-9! At that point, mid = (lo + hi) * 0.5 rounds to either exact lo or exact hi, and while (hi - lo > 1e-9) spins in an infinite loop forever. Golden Rule: For floating-point binary search, always use a fixed iteration count (for (int iter = 0; iter < 70; ++iter)), which halves the interval $2^{70} \approx 10^{21}$ times down to machine epsilon and guarantees termination!

Production C++20 Implementation: Discrete Min-Max Partition & Continuous 0-1 Fractional Search

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <numeric>
#include <vector>

// 1. Discrete Parametric Search: Split array w of non-negative ints into <= k contiguous subarrays
//    minimizing the maximum subarray sum. Time: O(N log(sum(w))), Space: O(1).
std::int64_t minimize_max_subarray_sum(const std::vector<std::int64_t>& w, std::size_t k) noexcept {
    std::int64_t lo = *std::max_element(w.begin(), w.end());
    std::int64_t hi = std::accumulate(w.begin(), w.end(), std::int64_t{0});

    auto feasible = [&](std::int64_t cap) noexcept -> bool {
        std::size_t buckets = 1;
        std::int64_t cur = 0;
        for (std::int64_t x : w) {
            if (cur + x > cap) {
                ++buckets;
                cur = x;
                if (buckets > k) return false; // Early exit!
            } else {
                cur += x;
            }
        }
        return true;
    };

    while (lo < hi) {
        const std::int64_t mid = lo + (hi - lo) / 2;
        if (feasible(mid)) hi = mid;
        else               lo = mid + 1;
    }
    return lo;
}

// 2. Continuous Parametric Search (0-1 Fractional Programming):
//    Choose exactly k items to maximize (sum a_i) / (sum b_i) where b_i > 0.
double maximize_ratio_subset(const std::vector<double>& a,
                             const std::vector<double>& b,
                             std::size_t k) {
    const std::size_t n = a.size();
    double lo = 0.0, hi = 0.0;
    for (std::size_t i = 0; i < n; ++i) hi = std::max(hi, a[i] / b[i]);

    std::vector<double> residuals(n);
    // Fixed 64 iterations: immune to IEEE-754 ULP underflow infinite loops!
    for (int iter = 0; iter < 64; ++iter) {
        const double mid = lo + 0.5 * (hi - lo);
        for (std::size_t i = 0; i < n; ++i) {
            residuals[i] = a[i] - mid * b[i];
        }
        // Use O(N) Introselect (nth_element) instead of O(N log N) sort to sum top k residuals!
        std::nth_element(residuals.begin(),
                         residuals.begin() + static_cast<std::ptrdiff_t>(k),
                         residuals.end(),
                         std::greater<double>{});
        double top_k_sum = 0.0;
        for (std::size_t i = 0; i < k; ++i) top_k_sum += residuals[i];

        if (top_k_sum >= 0.0) lo = mid;
        else                  hi = mid;
    }
    return lo;
}
When Parametric Search Fails: Non-Monotonic Predicates & Negative Weights

Before applying Parametric Search, prove two things:
1. Monotonicity of the Answer Space: Does $X$ always stay feasible for all $X' > X$? (For example, "Does there exist a subarray of exact sum $X$?" is not monotonic in $X$).
2. Correctness of the Greedy Oracle: In "Split Array Largest Sum", the greedy oracle if (cur + x > cap) works only because all weights $w_i \ge 0$! If $w_i$ can be negative, extending a subarray further to the right might decrease its sum, so greedy left-to-right packing is no longer valid even though the global feasibility predicate is still monotonic in $X$!

Check yourself Question 1 of 4
Concept Check Necessary and Sufficient Condition for Parametric Search recall

You want to find the minimum capacity X required to complete a scheduling task within K days by binary searching over X in [lo, hi].

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

What causes the infinite loop, and what is the mathematically sound fix?

Key Takeaways

Parametric Search transforms min-max or max-min optimization problems into a binary search over candidate answers X coupled with a fast O(N) feasibility check.
In 0-1 Fractional Programming, testing whether (∑ a_i) / (∑ b_i) >= λ reduces to checking whether the top-k largest values of (a_i - λ b_i) sum to >= 0 (solvable in O(N) per step via std::nth_element).
For floating-point binary search, always run a fixed number of iterations (60–80) rather than while (hi - lo > eps) to prevent infinite loops when eps is smaller than 1 ULP.
Always verify that item weights satisfy the greedy oracle's precondition (e.g., w_i >= 0 for greedy prefix packing).
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue