Hard problems: P, NP and approximation
Why some problems have no known fast algorithm: P, NP and NP-completeness explained without the jargon, how to recognise a hard problem, and what to do instead: exact search for small inputs, pseudo-polynomial DP, approximation and heuristics.
| Easy (polynomial) | Looks similar, but hard |
|---|---|
P and NP in plain words
P is the class of yes/no problems that can be solved in polynomial time. Is there a route of at most 40 km from the yard to the site? Dijkstra answers that. NP is the class of yes/no problems where a proposed answer can be checked in polynomial time. Is there a round trip through all 20 sites of at most 100 km? Hard to find, but if someone hands you a round, you can add up its length in O(n) and check. Every problem in P is in NP (if you can solve it, you can check an answer). The great open question, P = NP?, asks whether every quickly checkable problem is also quickly solvable. Most researchers believe not, and a proof either way is worth a US$1 million Millennium Prize.
int[] loads = { 34, 4, 12, 5, 2, 27, 19, 8 }; // can a truck carry exactly 50?int target = 50; int[] claim = { 0, 1, 2 }; // someone claims: 34 + 4 + 12Console.WriteLine($"Claim checked: {claim.Sum(i => loads[i]) == target}"); // O(n) int solutions = 0;for (int mask = 0; mask < 1 << loads.Length; mask++) // finding one ourselves: every subset, 2^n{ int sum = 0; for (int i = 0; i < loads.Length; i++) if ((mask & (1 << i)) != 0) sum += loads[i]; if (sum == target) solutions++;}Console.WriteLine($"Searched {1 << loads.Length} subsets, {solutions} hit {target}");Claim checked: True Searched 256 subsets, 4 hit 50
NP-complete: the hardest of them
Exact for small n, good enough for large n
Small inputs: exact search. Brute force or backtracking with pruning handles n ≤ 10–12. Bitmask DP (Held-Karp for TSP) is O(n²·2ⁿ), good up to about 20–25. Small numbers: pseudo-polynomial DP. 0/1 knapsack is O(n·W): fast when the capacity W is a few thousand, even though it is exponential in the number of digits of W. Approximation with a guarantee. Some algorithms promise to be within a factor of the optimum: the vertex cover method below is at most 2× optimal; Christofides’ algorithm for TSP with real distances is at most 1.5×. Heuristics and local search. Greedy construction plus improvement (2-opt, simulated annealing, genetic algorithms) gives excellent answers in practice with no guarantee. Special structure. The general problem is hard, but yours may not be: colouring is easy on trees, many problems are polynomial on graphs with small treewidth. Solvers. SAT solvers and integer linear programming solvers (such as Google OR-Tools) solve industrial instances with thousands of variables.
int[,] d = // driving minutes between the yard (0) and 4 sites{ { 0, 12, 10, 19, 8 }, { 12, 0, 3, 7, 2 }, { 10, 3, 0, 6, 20 }, { 19, 7, 6, 0, 4 }, { 8, 2, 20, 4, 0 }};int n = d.GetLength(0), full = 1 << n, inf = int.MaxValue / 2;var best = new int[full, n]; // best[set, j]: shortest path from 0 visiting set, ending at jfor (int s = 0; s < full; s++) for (int j = 0; j < n; j++) best[s, j] = inf;best[1, 0] = 0; for (int s = 1; s < full; s += 2) // every set containing the yard (bit 0) for (int j = 0; j < n; j++) { if ((s & (1 << j)) == 0 || best[s, j] == inf) continue; for (int k = 0; k < n; k++) if ((s & (1 << k)) == 0) // extend the path to a new site k { int t = s | (1 << k); best[t, k] = Math.Min(best[t, k], best[s, j] + d[j, k]); } } int answer = inf;for (int j = 1; j < n; j++) answer = Math.Min(answer, best[full - 1, j] + d[j, 0]);Console.WriteLine($"Shortest round: {answer} minutes");Console.WriteLine($"DP states: {full * n}, brute-force orders: {Factorial(n - 1)}"); static long Factorial(int k) => k <= 1 ? 1 : k * Factorial(k - 1);Shortest round: 32 minutes DP states: 160, brute-force orders: 24
An approximation with a guarantee
var corridors = new[] { ("A", "B"), ("A", "C"), ("B", "C"), ("B", "D"), ("C", "E"), ("D", "E"), ("D", "F") }; var cover = new HashSet<string>();foreach (var (u, v) in corridors) if (!cover.Contains(u) && !cover.Contains(v)) { cover.Add(u); cover.Add(v); } // guard both endsConsole.WriteLine($"2-approximation: {string.Join(", ", cover.Order())} ({cover.Count})"); var junctions = corridors.SelectMany(c => new[] { c.Item1, c.Item2 }).Distinct().ToArray();int best = junctions.Length;string bestSet = "";for (int mask = 0; mask < 1 << junctions.Length; mask++) // exact: try every subset{ var set = junctions.Where((_, i) => (mask & (1 << i)) != 0).ToHashSet(); if (set.Count < best && corridors.All(c => set.Contains(c.Item1) || set.Contains(c.Item2))) { best = set.Count; bestSet = string.Join(", ", set.Order()); }}Console.WriteLine($"Optimal: {bestSet} ({best})");2-approximation: A, B, C, D, E, F (6) Optimal: A, C, D (3)
Show a hint
visited array. At each step scan all places for the unvisited one with the smallest d[here, k].
Show the solution
int[,] d ={ { 0, 12, 10, 19, 8 }, { 12, 0, 3, 7, 2 }, { 10, 3, 0, 6, 20 }, { 19, 7, 6, 0, 4 }, { 8, 2, 20, 4, 0 }};int n = d.GetLength(0), here = 0, total = 0;var visited = new bool[n];visited[0] = true;var route = new List<int> { 0 };for (int step = 1; step < n; step++){ int next = -1; for (int k = 0; k < n; k++) if (!visited[k] && (next < 0 || d[here, k] < d[here, next])) next = k; total += d[here, next]; visited[next] = true; route.Add(next); here = next;}total += d[here, 0]; // drive back to the yardroute.Add(0);Console.WriteLine($"{string.Join(" -> ", route)}: {total} minutes (optimum 32)");0 -> 4 -> 1 -> 2 -> 3 -> 0: 38 minutes (optimum 32)
Remember
P: solvable in polynomial time. NP: answers checkable in polynomial time. Whether P = NP is unknown. NP-complete problems (TSP, knapsack, subset sum, colouring, SAT) are equally hard: a fast algorithm for one would solve all. Recognise them by “best subset, order or assignment” among exponentially many options. In practice: exact search for small n, pseudo-polynomial DP, approximations with guarantees, heuristics and solvers.