GTOIgraph theory, redesigned

Chapter 9 · Hardness and Escape Routes

Escape Routes: What To Do When It Is NP-Hard

Small n, meet in the middle, branch and bound, FPT by a parameter, and approximation — with the running-time arithmetic for each.

  • hard
  • 2 snippets
  • exponential
  • DP
  • branch and bound

Five techniques, ranked by how the constraints tell you which one is intended.

#1. DP over subsets — n ≤ 22

dp[M][v]: 2n · n states, O(n) (or O(deg)) each.

cppsubset-dp.cpp
for (int M = 1; M < (1 << n); M++)
    for (int v = 0; v < n; v++) if (M >> v & 1) {
        int P = M ^ (1 << v);
        for (int u = 0; u < n; u++) if (P >> u & 1 && adj[v][u])
            dp[M][v] = min(dp[M][v], dp[P][u] + w(u, v));
    }

Arithmetic to check first: 222 · 22 ≈ 9 · 107 states — fine; 226 · 26 ≈ 1.7 · 109 — too slow and memory 226 · 26 bytes = 1.7 GB, too big. Memory is usually the binding constraint: iterate masks and keep only dp[mask][v] as int (4 bytes), or drop the [v] dimension entirely when the transition depends only on M (then O(2n) memory).

Variants of the same 8 lines

  • Hamiltonian path/cycle and TSP (Held–Karp: Hamilton in O(2ⁿn²)),
  • assignment problem with n ≤ 20 (faster than Hungarian for tiny n),
  • "minimum number of groups with a constraint on each" — iterate subsets, dp[M] = min over sub ⊆ M, which is O(3n) total: enumerate submasks with for (sub = M; sub; sub = (sub-1) & M),
  • graph colouring in O(2n n): precompute which subsets are independent, then "cover V by ≤ k independent sets" = subset DP, or use inclusion–exclusion for the chromatic polynomial,
  • Steiner tree: dp[M][v] over terminal subsets with Dijkstra between layers, O(3t n + 2t n log n).

#2. Meet in the middle — n ≤ 44

Split into two halves, enumerate 2n/2 subsets each, then combine by sorting + two pointers or binary search.

cppmeet-in-middle.cpp
// subset sum to target T with n <= 40
vector<ll> A, B;                       // all 2^(n/2) subset sums of each half
sort(B.begin(), B.end());
ll best = -1;
for (ll a : A) {
    auto it = lower_bound(B.begin(), B.end(), T - a);
    if (it != B.end() && a + *it == T) { best = T; break; }
}

2 · 220 = 2 · 106 elements instead of 240 — the entire trick is that a sorted list can be searched, so pairs become cheap.

#3. Branch and bound / backtracking with a real prune — n ≤ 40–60 on nice tests

The prunes that matter, in order of value:

  1. 1

    order the branching so that the strongest constraint is decided first (largest degree, smallest domain);

  2. 2

    bound with a fast relaxation (LP-free: fractional knapsack bound, MST bound for TSP, greedy colour count) and cut the subtree when the bound cannot beat the incumbent,

  3. 3

    forward checking: after assigning, remove impossible values; fail immediately on an empty domain,

  4. 4

    memoise on a canonical state (a bitmask of decided vertices) — the moment you memoise, branch and bound becomes DP and the bound stops mattering,

  5. 5

    restart / randomise the order if a fixed order blows up on one adversarial test.

ExampleMaximal independent set in a graph with n = 50

Branch on a vertex v: either take it (delete N[v]) or forbid it (delete v). Recurrence T(n) = T(n-1) + T(n-d-1); with d ≥ 2 that is ≈ 1.32n. Measure-and-conquer analyses push it to 1.1996n by handling low-degree vertices with dedicated rules — which is exactly what the "branch on the degree-2 vertex and contract it" line in real code is doing.

#4. FPT in a parameter you noticed

"Exponential in k, polynomial in n" — kernelise then branch:

  • vertex cover: bounded search tree O(2k k), or O(1.2738k + kn) with branching rules on degree-3+ vertices; LP relaxation gives a 2k-vertex kernel,
  • treewidth k: solve any MSO/DP problem in O(f(k) n) once you have a tree decomposition (and for planar graphs, k = O(√n), so "exponential in √n" is a real planar algorithm),
  • dominating set on planar graphs: O(2O(√n) n) via the same,
  • "at most k edges to delete to become bipartite": iterative compression or O(4k · n).

#5. Approximation and randomisation

Provably-good answers you can code in 5 lines

  • maximal matching → 2-approx for vertex cover and for maximum matching (The Extremal Principle),
  • random partition, keep the better side → ≥ m/2 edges in a bipartite subgraph; derandomise by greedy flipping, which is the same bound,
  • Christofides for metric TSP: MST + minimum perfect matching on the odd-degree vertices (3/2) — uses General Matching and Blossoms as a subroutine, which is why that page exists,
  • local search "improve by swapping 2" for TSP: O(1)-factor in practice, no proof,
  • simulated annealing / random restarts for "output any good answer" scoring problems: measure your own bound rather than trusting folklore.
NoteChoosing among the five, in one rule

Read n and k and compute. 2n fits → subset DP. 2n/2 fits → meet in the middle. n huge but k ≤ 20 → FPT/branch. n huge, no parameter, "output any" → approximation with a proof you can state. If none of the four fit, re-read the statement: you have mis-modelled the problem, which is by far the most common reason.