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.
- 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 ascur_sum + w[i] <= X; start a new bucket whenever it overflows. $X$ is feasible iff total buckets used $\le k$. - 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. - 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 |
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.
Invariant & State
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;
}
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$!