When exact optimization is NP-hard, settle for a provable ratio. An algorithm is a -approximation if on every input (minimization) or (maximization). Two lessons: the achievable ratio varies enormously between superficially similar problems, and many of the best ratios are provably optimal (assuming or the Unique Games Conjecture).

ProblemBest ratioHardness
vertex cover2NP-hard below ; under UGC
metric TSP (Christofides), (2021)no PTAS
Euclidean TSP (Arora’s PTAS)
general TSPnoneno constant factor unless P = NP
set cover (greedy) is NP-hard
max cut0.878 (Goemans–Williamson)optimal under UGC; NP-hard above
max SAT0.797
knapsackFPTAS
bin packing (FFD); asymptotic PTASabsolute NP-hard
makespan ( identical machines) (LPT); PTAS
max independent set: essentially inapproximable
Steiner tree
facility location (metric)1.4881.463
-center2 (farthest point) NP-hard
max coverage, monotone submodular with cardinality (greedy)optimal unless P = NP
ClassRunning timeExample
PTASpolynomial for each fixed (may be )Euclidean TSP, makespan
FPTASpolynomial in and knapsack, subset sum
APXsome constant factorvertex cover, metric TSP
no constant factorgeneral TSP, max clique

A strongly NP-hard problem has no FPTAS unless P = NP (an FPTAS with , OPT bounded by a polynomial in the numbers, would solve it exactly); knapsack has one because it is only weakly NP-hard (Complexity Theory).

Techniques

TechniqueIdeaExamples
greedy with a boundlocally best choices, charged against OPTset cover, vertex cover, -center, max coverage, LPT, FFD
lower-bounding OPTfind with MST for TSP, a matching for vertex cover
LP relaxation + roundingthe LP value bounds OPT; round the fractional optimumvertex cover (half-integral), set cover, scheduling on unrelated machines (Lenstra–Shmoys–Tardos, 2)
randomized roundingtreat fractional values as probabilities; Chernoff for constraintsset cover , congestion (Raghavan–Thompson), MAX-SAT
SDP + hyperplane roundingvectors instead of max cut 0.878, MAX-2SAT, MAX-DICUT, correlation clustering
primal–dualbuild a solution and a dual certificate together; weak duality gives the ratiofacility location, Steiner forest
local searchimprove while possible; sum the local-optimality inequalitiesmax cut , -median
DP + rounding (FPTAS)round numbers so a pseudo-polynomial DP becomes smallknapsack
matroids: greedy is exactno approximation neededspanning trees, linear bases, unit jobs with deadlines: Matroids

Proving a ratio, the standard shapes: charging (assign each unit of ALG’s cost to something OPT must pay for), lower-bounding OPT, local optimality (no improving move: sum those inequalities), LP duality (a feasible dual bounds OPT). Matroid intersection of two matroids is polynomial; of three it is NP-hard.

In contests approximation ratios rarely appear directly (problems want exact answers); the ideas help in marathon problems (a ratio justifies a construction), to bound a greedy’s quality and decide whether it can be optimal, to recognise APX-hard problems and stop searching for an exact polynomial algorithm, in branch and bound (an approximation is a good incumbent and pruning bound), and in constructive tasks that really ask for “any solution within of optimal”.

Greedy algorithms with guarantees

ProblemGreedy ruleRatio
set cover (weighted: best cost per new element)most uncovered elements
vertex coverboth endpoints of an uncovered edge2
metric TSPdouble the MST, shortcut2
metric TSPChristofides
max cutlocal search: move any vertex that improves the cut
makespanleast-loaded machine (any order) / LPT (longest first) /
bin packingfirst fit decreasing
-centerfarthest point (Gonzalez)2
max coverage ( sets), monotone submodularlargest marginal gain (Nemhauser–Wolsey–Fisher)
Steiner treeMST of the metric closure on the terminals2
knapsackratio order, or the best single item2

The "obvious" greedy is worse

For vertex cover, repeatedly taking the highest-degree vertex feels better but is only -approximate; taking both endpoints of an uncovered edge is 2-approximate. Intuition is not a proof. Similarly, nearest-neighbour TSP is only .

vector<int> greedySetCover(int n, const vector<vector<int>>& sets) {   // elements 0..n-1; H_n-approximation
    vector<char> covered(n, 0); vector<int> chosen; int remaining = n;
    while (remaining > 0) {
        int best = -1, bestGain = 0;
        for (int i = 0; i < sz(sets); i++) {
            int gain = 0; for (int e : sets[i]) gain += !covered[e];
            if (gain > bestGain) bestGain = gain, best = i;
        }
        if (best < 0) break;                                 // infeasible
        for (int e : sets[best]) if (!covered[e]) covered[e] = 1, remaining--;
        chosen.push_back(best);
    }
    return chosen;
}
 
vector<int> vertexCover2(int n, const vector<pair<int, int>>& edges) {   // 2-approximation (maximal matching)
    vector<char> in(n, 0); vector<int> cover;
    for (auto [u, v] : edges) if (!in[u] && !in[v]) { in[u] = in[v] = 1; cover.push_back(u); cover.push_back(v); }
    return cover;
}
 
vector<int> kCenterGonzalez(const vector<vector<double>>& d, int k) {   // 2-approximation of the min max radius
    int n = sz(d); vector<int> centers{0}; vector<double> dist = d[0];
    while (sz(centers) < min(k, n)) {
        int far = int(max_element(all(dist)) - dist.begin());
        centers.push_back(far);
        for (int i = 0; i < n; i++) dist[i] = min(dist[i], d[far][i]);
    }
    return centers;
}
 
ll lptMakespan(vector<ll> p, int m) {                        // longest processing time first
    sort(all(p), greater<ll>());
    priority_queue<ll, vector<ll>, greater<ll>> load; for (int i = 0; i < m; i++) load.push(0);
    for (ll x : p) { ll l = load.top(); load.pop(); load.push(l + x); }
    ll mx = 0; while (!load.empty()) mx = max(mx, load.top()), load.pop();
    return mx;
}
 
int firstFitDecreasing(vector<double> w) {                   // bins of capacity 1
    sort(all(w), greater<double>()); vector<double> bins;
    for (double x : w) {
        bool placed = false;
        for (auto& b : bins) if (b + x <= 1 + 1e-12) { b += x; placed = true; break; }
        if (!placed) bins.push_back(x);
    }
    return sz(bins);
}
 
vector<int> greedyMaxCoverage(int n, const vector<vector<int>>& sets, int k) {   // (1 - 1/e)-approximation
    vector<char> covered(n, 0); vector<int> chosen;
    for (int t = 0; t < k; t++) {
        int best = -1, bestGain = -1;
        for (int i = 0; i < sz(sets); i++) { int g = 0; for (int e : sets[i]) g += !covered[e]; if (g > bestGain) bestGain = g, best = i; }
        if (best < 0) break;
        for (int e : sets[best]) covered[e] = 1;
        chosen.push_back(best);
    }
    return chosen;
}

LP rounding, randomized rounding and the knapsack FPTAS

Vertex cover by LP: , on edges, . Every vertex of this polytope is half-integral (), so rounding up gives a 2-approximation directly (and the Nemhauser–Trotter kernel).

Randomized rounding (Raghavan–Thompson, 1987): (1) write an integer LP, (2) relax to and solve (the LP value bounds OPT), (3) set independently with probability , (4) the expected objective equals the LP value by linearity, and Chernoff bounds show constraints are nearly satisfied with high probability, (5) repair or repeat. For set cover, run independent rounds: each costs LP in expectation, and an element is missed in one round with probability , so after rounds all elements are covered with high probability (union bound): an -approximation, matching greedy and the hardness. The original application was global wire routing in VLSI: route many nets through a grid minimising maximum congestion; the fractional routing is a flow LP, and rounding each net to one random path keeps congestion within of optimal. Chernoff, in the form used there: , (Randomized Algorithms). Derandomization by conditional expectations with pessimistic estimators makes the rounding deterministic.

ProblemLP relaxationResult
set coverfractional cover
vertex coverfractional cover (half-integral)2, deterministic rounding at
congestion / multicommodity routingfractional flow, the original application
facility locationfractional assignmentconstant factor with clustered rounding
scheduling on unrelated machinesassignment LP2 (Lenstra–Shmoys–Tardos)
MAX-SATLP + biased coins
// (1 - eps)-approximation for 0/1 knapsack: scale values, then the value-indexed DP (min weight per value).
ll knapsackFPTAS(const vector<ll>& w, const vector<ll>& v, ll W, double eps) {
    int n = sz(w); ll vmax = 0; for (int i = 0; i < n; i++) if (w[i] <= W) vmax = max(vmax, v[i]);
    if (vmax == 0) return 0;
    double K = max(1.0, eps * vmax / n);
    vector<ll> sv(n); ll total = 0;
    for (int i = 0; i < n; i++) sv[i] = w[i] <= W ? (ll)(v[i] / K) : 0, total += sv[i];
    const ll INF = LLONG_MAX / 4; vector<ll> dp(total + 1, INF); vector<vector<char>> take(n, vector<char>(total + 1, 0)); dp[0] = 0;
    for (int i = 0; i < n; i++) if (w[i] <= W && sv[i] > 0)
        for (ll s = total; s >= sv[i]; s--) if (dp[s - sv[i]] + w[i] < dp[s]) dp[s] = dp[s - sv[i]] + w[i], take[i][s] = 1;
    ll best = 0; for (ll s = total; s >= 0; s--) if (dp[s] <= W) { best = s; break; }
    ll value = 0;                                            // recover the chosen items, return their true value
    for (int i = n - 1; i >= 0 && best > 0; i--) if (take[i][best]) value += v[i], best -= sv[i];
    return value;
}

Metric TSP: double tree and Christofides

Christofides (1976): (1) a minimum spanning tree ; (2) = vertices of odd degree in ( even by the handshake lemma); (3) a minimum-weight perfect matching on ( via weighted blossom); (4) has all degrees even: take an Euler circuit (Hierholzer); (5) shortcut repeated vertices (never more expensive by the triangle inequality). time, space. The double-tree algorithm skips the matching (double every MST edge): ratio 2 in . Without the triangle inequality TSP has no constant-factor approximation unless P = NP (set non-edges to a huge weight and a -approximation decides Hamiltonian cycle).

MethodRatioTime
nearest neighbour
double tree2
Christofides
Karlin–Klein–Oveis Gharan (2021)polynomial
Held–Karpexact: Hamiltonian Paths and TSP
2-opt / Or-opt / Lin–Kernighanno guaranteefast; within 1–2% of optimal on real instances: Heuristic Optimization

Nobody runs Christofides on real instances; it matters for the guarantee. The code below uses a bitmask DP for the matching, enough to verify the algorithm on small instances (use weighted blossom beyond about 20 odd vertices).

double tourCost(const vector<vector<double>>& d, const vector<int>& t) { double s = 0; for (int i = 0; i < sz(t); i++) s += d[t[i]][t[(i + 1) % sz(t)]]; return s; }
 
vector<int> christofides(const vector<vector<double>>& d) {   // metric d; matching by bitmask DP (<= ~20 odd vertices)
    int n = sz(d); if (n <= 2) { vector<int> t(n); iota(all(t), 0); return t; }
    vector<int> par(n, -1); vector<double> best(n, 1e18); vector<char> in(n, 0); best[0] = 0;
    vector<vector<int>> g(n);
    for (int it = 0; it < n; it++) {                         // Prim, O(n^2)
        int u = -1; for (int v = 0; v < n; v++) if (!in[v] && (u < 0 || best[v] < best[u])) u = v;
        in[u] = 1; if (par[u] >= 0) g[u].push_back(par[u]), g[par[u]].push_back(u);
        for (int v = 0; v < n; v++) if (!in[v] && d[u][v] < best[v]) best[v] = d[u][v], par[v] = u;
    }
    vector<int> odd; for (int v = 0; v < n; v++) if (sz(g[v]) % 2) odd.push_back(v);
    int k = sz(odd); vector<double> dp(1 << k, 1e18); vector<int> choice(1 << k, -1); dp[0] = 0;
    for (int mask = 0; mask < (1 << k); mask++) {            // pair the lowest unmatched odd vertex
        if (dp[mask] >= 1e17) continue;
        int i = 0; while (i < k && (mask >> i & 1)) i++;
        if (i == k) continue;
        for (int j = i + 1; j < k; j++) if (!(mask >> j & 1)) {
            int nm = mask | 1 << i | 1 << j; double c = dp[mask] + d[odd[i]][odd[j]];
            if (c < dp[nm]) dp[nm] = c, choice[nm] = i * k + j;
        }
    }
    for (int mask = (1 << k) - 1; mask; ) { int i = choice[mask] / k, j = choice[mask] % k; g[odd[i]].push_back(odd[j]); g[odd[j]].push_back(odd[i]); mask ^= 1 << i | 1 << j; }
    vector<int> st{0}, circuit; vector<multiset<int>> adj(n); for (int v = 0; v < n; v++) for (int u : g[v]) adj[v].insert(u);
    while (!st.empty()) {                                    // Hierholzer on the multigraph T + M
        int v = st.back();
        if (adj[v].empty()) { circuit.push_back(v); st.pop_back(); }
        else { int u = *adj[v].begin(); adj[v].erase(adj[v].begin()); adj[u].erase(adj[u].find(v)); st.push_back(u); }
    }
    vector<int> tour; vector<char> seen(n, 0);
    for (int v : circuit) if (!seen[v]) seen[v] = 1, tour.push_back(v);   // shortcut
    return tour;
}

Goemans–Williamson for MAX-CUT

MAX-CUT (partition the vertices to maximize the weight of crossing edges) is NP-hard and, unlike min cut, not solvable by flows. A random side for each vertex cuts each edge with probability (a 0.5-approximation in expectation), and local search also gives . Goemans–Williamson (1994): write with , relax each to a unit vector (a semidefinite program: the Gram matrix must be PSD, solvable to any precision in polynomial time by interior-point methods), then cut with a random hyperplane: for a Gaussian . Rounding costs per trial (repeat and keep the best); the SDP is polynomial but heavy (a few thousand vertices), so this is not a contest tool: for MAX-CUT-flavoured contest problems use local search or simulated annealing. The template it established (integer program, convex relaxation, random rounding, bound the loss) also gives MAX-2SAT, MAX-DICUT, correlation clustering, sparsest cut and colouring results. Under the Unique Games Conjecture no polynomial algorithm beats 0.878 (Khot, Kindler, Mossel, O’Donnell); unconditionally, is NP-hard.