Loading learning content...
We've established the theoretical foundation: ternary search solves optimization problems on unimodal functions where binary search fails. Now it's time to translate this understanding into working code.
This page provides complete, production-quality implementations of ternary search. We'll cover:
By the end, you'll have a robust toolkit for optimization problems in competitive programming and real-world applications.
By the end of this page, you will be able to implement ternary search from scratch for both continuous and discrete domains, handle precision issues correctly, choose appropriate termination conditions, and apply golden section search for improved efficiency.
Let's start with the most common case: finding the x-value that maximizes a unimodal function over a continuous interval [a, b].
Algorithm Overview
[left, right] = [a, b]m1 and m2 that divide the interval into thirdsf(m1) and f(m2)Why This Works
For a unimodal function with maximum at x*:
f(m1) < f(m2): Since f is increasing toward x* and decreasing after, x* must be to the right of m1. We can safely eliminate [left, m1].f(m1) > f(m2): Symmetrically, x* must be to the left of m2. Eliminate [m2, right].f(m1) = f(m2): Either point could be on either side of x*, but we can eliminate either outer third safely. Convention: eliminate the right third.Each iteration shrinks the interval to 2/3 of its previous size.
123456789101112131415161718192021222324252627282930313233343536373839404142434445
/** * Ternary Search: Find x that maximizes f(x) on continuous [left, right] * * @param f - The unimodal function to maximize * @param left - Left bound of search interval * @param right - Right bound of search interval * @param epsilon - Desired precision (default: 1e-9) * @returns x value that (approximately) maximizes f */function ternarySearchMax( f: (x: number) => number, left: number, right: number, epsilon: number = 1e-9): number { while (right - left > epsilon) { // Divide interval into thirds const m1 = left + (right - left) / 3; const m2 = right - (right - left) / 3; // Compare function values at interior points if (f(m1) < f(m2)) { // Maximum is to the right of m1 // Eliminate left third: [left, m1] left = m1; } else { // Maximum is to the left of m2 // Eliminate right third: [m2, right] right = m2; } } // Return midpoint of final interval return (left + right) / 2;} // Example: Find x that maximizes f(x) = -(x - 7)² + 100// Maximum should be at x = 7 where f(7) = 100function exampleFunc(x: number): number { return -Math.pow(x - 7, 2) + 100;} const xMax = ternarySearchMax(exampleFunc, 0, 20);console.log(`Maximum at x = ${xMax.toFixed(6)}`); // ~7.000000console.log(`f(x) = ${exampleFunc(xMax).toFixed(6)}`); // ~100.000000The points m1 and m2 divide [left, right] into three equal parts. m1 = left + (right - left)/3 is 1/3 of the way from left to right. m2 = right - (right - left)/3 is 2/3 of the way (equivalently, 1/3 from the right).
Finding the minimum of a unimodal function (a "valley" rather than "peak") requires only a small modification: we reverse the comparison.
For Maximum: If f(m1) < f(m2), eliminate left third (go toward higher values)
For Minimum: If f(m1) < f(m2), eliminate right third (go toward lower values)
Alternatively, you can reuse the maximum-finding code by negating the function: min f(x) = max -f(x).
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
/** * Ternary Search: Find x that minimizes f(x) on continuous [left, right] */function ternarySearchMin( f: (x: number) => number, left: number, right: number, epsilon: number = 1e-9): number { while (right - left > epsilon) { const m1 = left + (right - left) / 3; const m2 = right - (right - left) / 3; // REVERSED COMPARISON for minimum if (f(m1) > f(m2)) { // Minimum is to the right of m1 left = m1; } else { // Minimum is to the left of m2 right = m2; } } return (left + right) / 2;} // Example: Find x that minimizes f(x) = (x - 4)² + 5// Minimum should be at x = 4 where f(4) = 5function convexFunc(x: number): number { return Math.pow(x - 4, 2) + 5;} const xMin = ternarySearchMin(convexFunc, -10, 10);console.log(`Minimum at x = ${xMin.toFixed(6)}`); // ~4.000000console.log(`f(x) = ${convexFunc(xMin).toFixed(6)}`); // ~5.000000 // Alternative: Use max version with negated functionfunction ternarySearchMaxFromMin( f: (x: number) => number, left: number, right: number, epsilon: number = 1e-9): number { return ternarySearchMax(x => -f(x), left, right, epsilon);} // ternarySearchMaxFromMin returns same result by maximizing -f(x)Minimization and maximization are dual problems. Any algorithm that finds a maximum can find a minimum by negating the function (or flipping comparisons). This duality means you only need to truly understand one case.
Choosing when to terminate ternary search is subtle. Unlike discrete binary search which naturally terminates, continuous ternary search can refine indefinitely. Here are the main approaches:
Strategy 1: Absolute Epsilon
Terminate when right - left < epsilon for some fixed epsilon (e.g., 1e-9).
Strategy 2: Relative Epsilon
Terminate when (right - left) / |right + left| < epsilon.
Strategy 3: Fixed Iteration Count
Run exactly N iterations regardless of convergence.
Strategy 4: Combined Approach
Use relative epsilon with a fallback to absolute epsilon, plus a maximum iteration limit.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
// Different termination strategies for ternary search // Strategy 1: Absolute epsilonfunction ternarySearchAbsolute( f: (x: number) => number, left: number, right: number, absEps: number = 1e-9): number { while (right - left > absEps) { const m1 = left + (right - left) / 3; const m2 = right - (right - left) / 3; if (f(m1) < f(m2)) left = m1; else right = m2; } return (left + right) / 2;} // Strategy 2: Relative epsilonfunction ternarySearchRelative( f: (x: number) => number, left: number, right: number, relEps: number = 1e-9): number { while (true) { const mid = (left + right) / 2; if (Math.abs(mid) < 1e-15 || (right - left) / Math.abs(mid) <= relEps) { return mid; } const m1 = left + (right - left) / 3; const m2 = right - (right - left) / 3; if (f(m1) < f(m2)) left = m1; else right = m2; }} // Strategy 3: Fixed iterations (most common in competitive programming)function ternarySearchIterations( f: (x: number) => number, left: number, right: number, iterations: number = 200 // Usually sufficient for 1e-60 precision): number { for (let i = 0; i < iterations; i++) { const m1 = left + (right - left) / 3; const m2 = right - (right - left) / 3; if (f(m1) < f(m2)) left = m1; else right = m2; } return (left + right) / 2;} // Strategy 4: Combined approach (production-grade)function ternarySearchRobust( f: (x: number) => number, left: number, right: number, absEps: number = 1e-12, relEps: number = 1e-9, maxIter: number = 300): number { for (let i = 0; i < maxIter; i++) { const width = right - left; const mid = (left + right) / 2; // Check termination conditions if (width < absEps) break; if (Math.abs(mid) > 1e-15 && width / Math.abs(mid) < relEps) break; const m1 = left + width / 3; const m2 = right - width / 3; if (f(m1) < f(m2)) left = m1; else right = m2; } return (left + right) / 2;} // Demo: How many iterations for given precision?console.log("Iterations needed for precision:");console.log(`ε = 1e-6: ~${Math.ceil(Math.log(1e6) / Math.log(1.5))} iterations`);console.log(`ε = 1e-9: ~${Math.ceil(Math.log(1e9) / Math.log(1.5))} iterations`);console.log(`ε = 1e-12: ~${Math.ceil(Math.log(1e12) / Math.log(1.5))} iterations`);In contests, use the fixed iteration approach with 200-300 iterations. This gives precision well beyond any reasonable requirement (~10^-60 relative to initial interval) and has completely predictable performance. No need to worry about edge cases with epsilon comparisons.
Convergence Analysis
Each iteration reduces the interval to 2/3 of its previous size. After k iterations:
final_width = initial_width × (2/3)^k
To achieve precision ε:
(2/3)^k < ε / initial_width
k > log(initial_width / ε) / log(3/2)
k > log_{1.5}(initial_width / ε)
For initial_width = 10^9 and ε = 10^-9:
k > log_{1.5}(10^18) ≈ 102 iterations
200 iterations provides a massive safety margin.
When the domain is integers (as in many competitive programming problems), ternary search needs slight modifications:
m1 and m2right - left < 3 (can't place two distinct interior points)The key insight is that with integers, when right - left = 2, we have exactly 3 candidates: left, left+1, right. We can't place two distinct interior points, so we must compare all three directly.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
/** * Ternary Search for integers: Find integer x that maximizes f(x) */function ternarySearchMaxInt( f: (x: number) => number, left: number, right: number): number { // Phase 1: Ternary search until only a few candidates remain while (right - left >= 3) { const m1 = left + Math.floor((right - left) / 3); const m2 = right - Math.floor((right - left) / 3); if (f(m1) < f(m2)) { left = m1; // Note: m1 itself might be the answer! } else { right = m2; } } // Phase 2: Linear scan of remaining candidates let bestX = left; let bestVal = f(left); for (let x = left + 1; x <= right; x++) { const val = f(x); if (val > bestVal) { bestVal = val; bestX = x; } } return bestX;} /** * Ternary Search for integers: Find integer x that minimizes f(x) */function ternarySearchMinInt( f: (x: number) => number, left: number, right: number): number { while (right - left >= 3) { const m1 = left + Math.floor((right - left) / 3); const m2 = right - Math.floor((right - left) / 3); if (f(m1) > f(m2)) { // Reversed for minimum left = m1; } else { right = m2; } } let bestX = left; let bestVal = f(left); for (let x = left + 1; x <= right; x++) { const val = f(x); if (val < bestVal) { // Reversed for minimum bestVal = val; bestX = x; } } return bestX;} // Example: Find k ∈ {0, 1, ..., 100} that minimizes f(k) = |k - 37|const targetK = ternarySearchMinInt(k => Math.abs(k - 37), 0, 100);console.log(`Answer: k = ${targetK}`); // 37 // Example: Revenue optimization with discrete prices// price ∈ {0, 1, 2, ..., 100}, revenue = price × (100 - price)const optimalPrice = ternarySearchMaxInt( price => price * (100 - price), 0, 100);console.log(`Optimal price: ${optimalPrice}`); // 50In integer ternary search, when we set left = m1, we're NOT eliminating m1 itself—m1 might be the answer. This differs from the continuous case where boundaries are infinitesimally refined. The final linear scan handles this correctly.
Standard ternary search evaluates f at two points per iteration. But there's a clever optimization: golden section search positions the probe points such that one evaluation from the previous iteration can be reused.
The Golden Ratio
The golden ratio φ = (1 + √5) / 2 ≈ 1.618 has a remarkable property: if we divide an interval by φ, the ratio of the whole to the larger part equals the ratio of the larger part to the smaller part.
How Golden Section Works
Place probe points at distances gr and 1-gr from the left, where gr = 2 - φ ≈ 0.382:
m1 = left + (right - left) × grm2 = left + (right - left) × (1 - gr)When we eliminate one third and shift the interval:
This reduction from 2 to 1 evaluation per iteration halves the number of function calls!
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
/** * Golden Section Search: Find x that maximizes f(x) * More efficient than standard ternary search - uses only 1 evaluation per iteration */function goldenSectionMax( f: (x: number) => number, left: number, right: number, epsilon: number = 1e-9): number { // Golden ratio constants const phi = (1 + Math.sqrt(5)) / 2; const gr = 2 - phi; // ≈ 0.381966... // Initial probe points let m1 = left + gr * (right - left); let m2 = right - gr * (right - left); let f1 = f(m1); let f2 = f(m2); while (right - left > epsilon) { if (f1 < f2) { // Maximum is in [m1, right] left = m1; m1 = m2; f1 = f2; // Reused m1/f1, only compute new m2/f2 m2 = right - gr * (right - left); f2 = f(m2); } else { // Maximum is in [left, m2] right = m2; m2 = m1; f2 = f1; // Reused m2/f2, only compute new m1/f1 m1 = left + gr * (right - left); f1 = f(m1); } } return (left + right) / 2;} // Example: Same function as beforefunction exampleFunc(x: number): number { return -Math.pow(x - 7, 2) + 100;} // Count function evaluations for comparisonlet ternaryEvals = 0;let goldenEvals = 0; function countedFuncTernary(x: number): number { ternaryEvals++; return exampleFunc(x);} function countedFuncGolden(x: number): number { goldenEvals++; return exampleFunc(x);} // Run both with same precisionconst eps = 1e-9;ternarySearchMax(countedFuncTernary, 0, 20, eps);goldenSectionMax(countedFuncGolden, 0, 20, eps); console.log(`Ternary search evaluations: ${ternaryEvals}`); // ~144console.log(`Golden section evaluations: ${goldenEvals}`); // ~74 (roughly half!)| Aspect | Ternary Search | Golden Section Search |
|---|---|---|
| Evaluations per iteration | 2 | 1 (after initialization) |
| Interval reduction factor | 2/3 ≈ 0.667 | 1/φ ≈ 0.618 |
| Total evaluations (N iterations) | 2N | N + 1 |
| Implementation complexity | Simple | Slightly more complex |
| Numerical stability | Excellent | Excellent |
| Best for | General use | Expensive function evaluations |
Use golden section search when function evaluation is expensive (database queries, simulations, external API calls). For simple mathematical functions, the overhead of the additional bookkeeping may not justify the savings. In competitive programming, standard ternary search is usually sufficient.
Here's a production-ready ternary search module that handles all common cases:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
/** * Complete Ternary Search Module * Production-ready implementations for all common use cases */ // ========================================// CONTINUOUS DOMAIN// ======================================== export function ternaryMaxContinuous( f: (x: number) => number, left: number, right: number, options: { epsilon?: number; maxIter?: number } = {}): { x: number; value: number } { const { epsilon = 1e-9, maxIter = 300 } = options; for (let i = 0; i < maxIter && right - left > epsilon; i++) { const m1 = left + (right - left) / 3; const m2 = right - (right - left) / 3; if (f(m1) < f(m2)) left = m1; else right = m2; } const x = (left + right) / 2; return { x, value: f(x) };} export function ternaryMinContinuous( f: (x: number) => number, left: number, right: number, options: { epsilon?: number; maxIter?: number } = {}): { x: number; value: number } { // Minimize f = maximize -f const result = ternaryMaxContinuous(x => -f(x), left, right, options); return { x: result.x, value: f(result.x) };} // ========================================// DISCRETE (INTEGER) DOMAIN// ======================================== export function ternaryMaxDiscrete( f: (x: number) => number, left: number, right: number): { x: number; value: number } { while (right - left >= 3) { const m1 = left + Math.floor((right - left) / 3); const m2 = right - Math.floor((right - left) / 3); if (f(m1) < f(m2)) left = m1; else right = m2; } let bestX = left; let bestVal = f(left); for (let x = left + 1; x <= right; x++) { const val = f(x); if (val > bestVal) { bestVal = val; bestX = x; } } return { x: bestX, value: bestVal };} export function ternaryMinDiscrete( f: (x: number) => number, left: number, right: number): { x: number; value: number } { while (right - left >= 3) { const m1 = left + Math.floor((right - left) / 3); const m2 = right - Math.floor((right - left) / 3); if (f(m1) > f(m2)) left = m1; else right = m2; } let bestX = left; let bestVal = f(left); for (let x = left + 1; x <= right; x++) { const val = f(x); if (val < bestVal) { bestVal = val; bestX = x; } } return { x: bestX, value: bestVal };} // ========================================// GOLDEN SECTION (EFFICIENT FOR EXPENSIVE F)// ======================================== export function goldenMaxContinuous( f: (x: number) => number, left: number, right: number, epsilon: number = 1e-9): { x: number; value: number } { const phi = (1 + Math.sqrt(5)) / 2; const gr = 2 - phi; let m1 = left + gr * (right - left); let m2 = right - gr * (right - left); let f1 = f(m1), f2 = f(m2); while (right - left > epsilon) { if (f1 < f2) { left = m1; m1 = m2; f1 = f2; m2 = right - gr * (right - left); f2 = f(m2); } else { right = m2; m2 = m1; f2 = f1; m1 = left + gr * (right - left); f1 = f(m1); } } const x = (left + right) / 2; return { x, value: f(x) };} // ========================================// USAGE EXAMPLES// ======================================== // Example 1: Continuous maximumconst result1 = ternaryMaxContinuous(x => -(x - 5) ** 2 + 100, 0, 10);console.log(`Continuous max: x=${result1.x.toFixed(4)}, f(x)=${result1.value.toFixed(4)}`); // Example 2: Discrete minimumconst result2 = ternaryMinDiscrete(x => (x - 42) ** 2, 0, 100);console.log(`Discrete min: x=${result2.x}, f(x)=${result2.value}`); // Example 3: Golden section for expensive functionlet callCount = 0;function expensiveF(x: number): number { callCount++; // Simulate expensive computation return -x ** 2 + 10 * x - 5;}const result3 = goldenMaxContinuous(expensiveF, 0, 10);console.log(`Golden section: x=${result3.x.toFixed(4)}, calls=${callCount}`);== to compare floats. Use epsilon-based comparison or sufficient iterations instead.Math.floor() for integer division. JavaScript / gives floats even for integers!right - left < 3 but not checking remaining candidates.Debugging Strategy
When ternary search gives unexpected results:
f(x) = -(x-5)² where you know the answer should be x=5.Create a test suite with known unimodal functions: parabolas, triangle functions, Gaussians. Verify the algorithm finds the correct extremum within expected precision. Include edge cases: very large ranges, very small ranges, answer at boundary, answer near zero.
We've covered complete, production-ready implementations of ternary search for finding extrema in unimodal functions.
What's Next:
We've mastered the algorithm itself. The next page compares ternary search with binary search in depth—when to use each, their relative efficiency, and how to recognize which approach fits a given problem. This comparison cements your understanding and gives you clear decision criteria.
You now have complete implementations for ternary search across continuous and discrete domains, with optimization techniques and debugging strategies. Next, we'll compare ternary search with binary search to understand exactly when each applies.