N or P
Contents Glossary
34

The approximation hierarchy — how good can you get

The second layer of the complexity map: not all "approximable" problems are equally approximable.

Approximation algorithms have their own complexity hierarchy. FPTAS — Fully Polynomial-Time Approximation Scheme — is the gold standard: tunable closeness to optimal with polynomial cost in both input size and the tightness. Knapsack has an FPTAS. PTAS is the next tier: polynomial cost in input size, but the polynomial may explode as the tightness increases. Euclidean TSP has a PTAS. APX is the class of problems with constant-factor approximations. APX-hard means no PTAS exists unless P = NP — vertex cover, metric TSP, set cover are APX-hard. Some problems (independent set, clique) cannot even be approximated within constant factors. For business planning, knowing where a problem sits in this hierarchy determines how much approximation budget you have. An FPTAS lets you trade compute for quality smoothly. APX-hard problems have a fixed quality ceiling — you can hit the bound but not improve below it without breaking complexity assumptions.

  1. 01
    The knapsack FPTAS — set an epsilon tolerance, scale every value down, run the DP on the smaller numbers, round back up. The result is within (1 − ε) of optimal in time polynomial in both the input and 1/ε. A quality knob you actually get to turn.
    fn knapsack_fptas(weights: &[u32], values: &[u32], capacity: u32, epsilon: f64) -> u64 {
        let v_max = *values.iter().max().unwrap_or(&1) as f64;
        let n = values.len() as f64;
        let k = (epsilon * v_max / n).max(1.0);
        let scaled: Vec<u32> = values.iter().map(|&v| (v as f64 / k) as u32).collect();
        let total: u32 = scaled.iter().sum();
        let mut dp = vec![u32::MAX; (total + 1) as usize];
        dp[0] = 0;
        for (i, &v) in scaled.iter().enumerate() {
            for s in (v..=total).rev() {
                if dp[(s - v) as usize] != u32::MAX {
                    dp[s as usize] = dp[s as usize].min(dp[(s - v) as usize] + weights[i]);
                }
            }
        }
        (0..=total)
            .filter(|&s| dp[s as usize] <= capacity)
            .map(|s| (s as f64 * k) as u64)
            .max()
            .unwrap_or(0)
    }
  2. 02
    *values.iter().max().unwrap_or(&1) is a four-step chain that hides a lot of work in one line. Read it right-to-left. values.iter().max() walks the slice and returns the maximum, but as Option<&u32> — None if the slice is empty, Some(&max) otherwise. The Option exists because a max-of-an-empty-collection has no answer. .unwrap_or(&1) provides a default: if max() returned Some(&v) we keep &v, and if it returned None we substitute &1 so the rest of the chain has a value to work with. The fallback type has to match what max yields — &u32 — so it is &1, not just 1. Finally, the leading * dereferences the resulting &u32 back to a plain u32 so it can be cast to f64 for the subsequent floating-point arithmetic. Same chain in nine words: "the largest value, or 1 if there are none."
    // Read right-to-left to follow the type at each step:
    //
    //   *values.iter().max().unwrap_or(&1) as f64
    //   ────────────────────────────────── ────
    //             u32                       f64
    //
    //   values             : &[u32]
    //   values.iter()      : iterator of &u32
    //   .max()             : Option<&u32>       — Option, because slice may be empty
    //   .unwrap_or(&1)     : &u32                — Some(&v) kept; None substitutes &1
    //   *                  : u32                 — dereference the reference
    
    
    // Step by step on a concrete slice:
    let values: &[u32] = &[3, 7, 2, 9, 4];
    
    let m: Option<&u32> = values.iter().max();   // Some(&9)
    let m: &u32         = m.unwrap_or(&1);        // &9
    let m: u32          = *m;                      // 9
    
    
    // Why each piece is needed:
    //
    //   .iter()         iterating a borrowed slice yields REFERENCES (&u32),
    //                    not owned values — the slice doesn't own the data
    //                    and can't hand out copies; it hands out views.
    //
    //   .max()          returns Option<T> because the iterator might be
    //                    empty.  No max exists for an empty collection.
    //
    //   .unwrap_or(&1)  gives a fallback for the None case.  The type must
    //                    match what max yields, so &1 (a reference), not 1.
    //                    Defaulting to 1 here prevents downstream division
    //                    by zero when computing the scale factor k.
    //
    //   *               collapse &u32 → u32 so the value can be cast to f64.
    
    
    // Equivalent with a match expression — more verbose, same behaviour:
    let v_max: f64 = match values.iter().max() {
        Some(&v) => v as f64,
        None     => 1.0,
    };
    
    
    // Equivalent with .copied() (page 18) to strip the reference earlier:
    let v_max: f64 = values
        .iter()
        .copied()                            // iterator of u32 instead of &u32
        .max()                                // Option<u32>
        .unwrap_or(1) as f64;                 // u32, no deref needed
    
    
    // Family of "unwrap with a fallback" methods on Option<T>:
    //
    //   .unwrap_or(default)         supply a fallback value directly
    //   .unwrap_or_else(|| compute) compute the fallback lazily (only on None)
    //   .unwrap_or_default()         use T's Default::default() value (0, "", etc.)
    //
    // .unwrap_or_else is the right choice when the fallback is expensive to
    // build — the closure only runs in the None case.
Arora, Lund, Motwani, Sudan, Szegedy (1998) the PCP theorem — the theoretical engine behind approximation limits. Source →
In plain terms

Not all NP-hard problems are equally approximable. The field has a hierarchy of labels that tells you, for any given hard problem, how close to optimal you can hope to get in polynomial time.

The gold standard is FPTAS — Fully Polynomial-Time Approximation Scheme. This means: for any quality target you specify (90% of optimal, 99%, 99.9%), there is an algorithm that achieves it in time polynomial in both the input size and the inverse of the gap. You get a quality knob to turn, and turning it tighter only costs polynomially more. Knapsack has an FPTAS — for any tolerance, an algorithm exists.

One step down is PTAS — Polynomial-Time Approximation Scheme. Same idea, but the polynomial can have the inverse gap in the exponent. This means tightening the approximation makes the algorithm exponentially slower in the tightness, even though it stays polynomial in input size. Euclidean TSP has a PTAS. PTAS is good news but with a caveat — production users typically pick a fixed tolerance and stick with it.

APX is the class of problems with some constant-factor approximation — within 2 of optimal, or 50% of optimal, or whatever. No quality knob, just one fixed ratio. Many practical hard problems sit here.

APX-hard is the bad news. These problems cannot be approximated arbitrarily closely unless P = NP. Vertex cover is APX-hard at the 2-approximation barrier (no PTAS unless P = NP, though small improvements below 2 are not ruled out yet). Metric TSP is APX-hard. Set cover is APX-hard at the logarithmic barrier.

Then there are problems that resist constant-factor approximation entirely. Independent set, clique, graph-coloring" data-term="graph coloring">graph coloring — these have no polynomial-time approximation within any constant factor unless P = NP. For these problems, the right approach is heuristic with no guarantees (page 36).

For business planning, the hierarchy tells you the engineering reality. An FPTAS problem is one where the quality-versus-compute trade-off is yours to tune. A PTAS problem requires committing to a tolerance upfront. An APX problem has one ratio and you take it or leave it. An APX-hard problem cannot improve below its known bound without a breakthrough. An inapproximable problem has no theoretical floor at all.

The engine behind all of this is the PCP theorem from 1998, which characterizes NP via probabilistic verification and lets you prove inapproximability bounds. Most working engineers do not need the details, but knowing the labels exist is enough to size the work correctly.

← 33 Approximation — provably close to optimal
34/39
35 When one dimension is small →