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).
| Problem | Best ratio | Hardness |
|---|---|---|
| vertex cover | 2 | NP-hard below ; under UGC |
| metric TSP | (Christofides), (2021) | no PTAS |
| Euclidean TSP | (Arora’s PTAS) | |
| general TSP | none | no constant factor unless P = NP |
| set cover | (greedy) | is NP-hard |
| max cut | 0.878 (Goemans–Williamson) | optimal under UGC; NP-hard above |
| max SAT | 0.797 | |
| knapsack | FPTAS | |
| bin packing | (FFD); asymptotic PTAS | absolute NP-hard |
| makespan ( identical machines) | (LPT); PTAS | |
| max independent set | : essentially inapproximable | |
| Steiner tree | ||
| facility location (metric) | 1.488 | 1.463 |
| -center | 2 (farthest point) | NP-hard |
| max coverage, monotone submodular with cardinality | (greedy) | optimal unless P = NP |
| Class | Running time | Example |
|---|---|---|
| PTAS | polynomial for each fixed (may be ) | Euclidean TSP, makespan |
| FPTAS | polynomial in and | knapsack, subset sum |
| APX | some constant factor | vertex cover, metric TSP |
| no constant factor | general 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
| Technique | Idea | Examples |
|---|---|---|
| greedy with a bound | locally best choices, charged against OPT | set cover, vertex cover, -center, max coverage, LPT, FFD |
| lower-bounding OPT | find with | MST for TSP, a matching for vertex cover |
| LP relaxation + rounding | the LP value bounds OPT; round the fractional optimum | vertex cover (half-integral), set cover, scheduling on unrelated machines (Lenstra–Shmoys–Tardos, 2) |
| randomized rounding | treat fractional values as probabilities; Chernoff for constraints | set cover , congestion (Raghavan–Thompson), MAX-SAT |
| SDP + hyperplane rounding | vectors instead of | max cut 0.878, MAX-2SAT, MAX-DICUT, correlation clustering |
| primal–dual | build a solution and a dual certificate together; weak duality gives the ratio | facility location, Steiner forest |
| local search | improve while possible; sum the local-optimality inequalities | max cut , -median |
| DP + rounding (FPTAS) | round numbers so a pseudo-polynomial DP becomes small | knapsack |
| matroids: greedy is exact | no approximation needed | spanning 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
| Problem | Greedy rule | Ratio |
|---|---|---|
| set cover (weighted: best cost per new element) | most uncovered elements | |
| vertex cover | both endpoints of an uncovered edge | 2 |
| metric TSP | double the MST, shortcut | 2 |
| metric TSP | Christofides | |
| max cut | local search: move any vertex that improves the cut | |
| makespan | least-loaded machine (any order) / LPT (longest first) | / |
| bin packing | first fit decreasing | |
| -center | farthest point (Gonzalez) | 2 |
| max coverage ( sets), monotone submodular | largest marginal gain | (Nemhauser–Wolsey–Fisher) |
| Steiner tree | MST of the metric closure on the terminals | 2 |
| knapsack | ratio order, or the best single item | 2 |
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;
}Proof: the greedy ratios
Set cover, . When an element is covered, charge it (the chosen set’s unit cost spread over its new elements); the total charge is the number of sets chosen. If OPT uses sets, at any moment those sets cover the remaining elements, so some set covers at least of them and the greedy gain is . The element covered when remain is charged ; summing over gives . No polynomial algorithm achieves unless P = NP (Dinur–Steurer, 2014).
Vertex cover, 2. The edges whose both endpoints were taken form a matching ; every cover contains an endpoint of each matching edge, distinct for distinct edges, so . The output is a cover: an edge left uncovered would have been taken.
-center, 2. Let be the final maximum distance to the nearest centre and a point attaining it. The centres and are points pairwise at distance (each centre was the farthest point when chosen). Two of them share a cluster of an optimal solution, so their distance is at most (triangle inequality through that cluster’s centre): . Doing better than 2 is NP-hard (Hsu–Nemhauser).
LPT, . Let job (processing time ) finish last, and drop the jobs after it (this does not change the makespan and does not increase OPT). It started at most at the average load of the others, , so . If this is the bound. Otherwise every job exceeds , so an optimal schedule has at most two jobs per machine and at most jobs; then pairing the -th largest with the -th largest (padding with zeros) is optimal by an exchange argument, and LPT produces exactly this pairing (jobs on separate machines, then each next job onto the currently least-loaded machine, which holds the smallest of the first ). The plain least-loaded rule in any order gives by the same first inequality with .
Max coverage, . Let OPT cover and greedy cover after steps. The optimal sets cover at least uncovered elements, so one covers , and . After steps . The same argument works for any monotone submodular function (diminishing returns: shrinks as grows): coverage, influence spread, sensor placement, feature selection.
FFD satisfies (Johnson 1973, the tight additive constant by Dósa 2007); the proof is a long weighting argument and is quoted here. All routines were checked against exact optima by brute force on small random instances (set cover and max coverage up to 8 sets, vertex cover up to 12 vertices, -center up to 9 points, LPT up to 9 jobs, FFD up to 9 items).
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.
| Problem | LP relaxation | Result |
|---|---|---|
| set cover | fractional cover | |
| vertex cover | fractional cover (half-integral) | 2, deterministic rounding at |
| congestion / multicommodity routing | fractional flow | , the original application |
| facility location | fractional assignment | constant factor with clustered rounding |
| scheduling on unrelated machines | assignment LP | 2 (Lenstra–Shmoys–Tardos) |
| MAX-SAT | LP + 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;
}Proof: half-integrality, and the FPTAS guarantee
Half-integrality. Let be a vertex of the polytope with some value outside . Let , , and define by adding on and on . For small both stay in , and every edge constraint stays satisfied: a tight edge () with one endpoint in has the other in (changes cancel), and non-tight edges have slack. Then is not a vertex: contradiction. Rounding: every edge has an endpoint with , and the rounded cost is .
FPTAS. With (where , since any single fitting item is a solution), each scaled value loses less than 1, i.e. less than in true value. The DP finds the set maximizing the scaled value exactly; for the optimal set , . Scaled values are at most , so the DP has states per item and total. Checked against exact DP: the returned value is always .
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).
| Method | Ratio | Time |
|---|---|---|
| nearest neighbour | ||
| double tree | 2 | |
| Christofides | ||
| Karlin–Klein–Oveis Gharan (2021) | polynomial | |
| Held–Karp | exact | : Hamiltonian Paths and TSP |
| 2-opt / Or-opt / Lin–Kernighan | no guarantee | fast; 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;
}Proof: the 2 and 3/2 ratios
(a) : deleting an edge of the optimal tour leaves a spanning path, a spanning tree. Double tree: the doubled tree has an Euler circuit of cost , and shortcutting never increases the cost (triangle inequality): . (b) : shortcut the optimal tour to visit only , in tour order: a cycle of cost on an even number of vertices, which splits into two perfect matchings of ; the cheaper costs , and the minimum matching is no heavier. (c) has even degrees (odd-degree vertices get one matching edge each), so it is Eulerian (connected because spans), with cost ; shortcutting only helps. Checked against Held–Karp on random Euclidean instances with up to 11 points: the ratio never exceeded .
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.
Proof: 0.878
Two unit vectors at angle are separated by a random hyperplane with probability : project onto their plane, where the hyperplane becomes a uniformly random line through the origin, which separates them iff its direction falls in an arc of total angle out of . The SDP gains from that edge, so each edge’s expected contribution is at least times its SDP contribution (minimum near rad, ). By linearity, , since the SDP is a relaxation.
Related notes
- Complexity Theory (inapproximability)
- Greedy and Exchange Arguments
- Linear Programming (duality, total unimodularity)