Easy to check, hard to find
A school in Nairobi has to schedule 40 examinations into a few time slots so that no student sits two exams at once. Draw a graph with one vertex per exam and an edge whenever some student takes both: a timetable with slots is exactly a colouring of the vertices with colours in which adjacent vertices differ. If someone hands you a proposed timetable, checking it is quick — look at every edge once. Finding one, or proving that none exists with three slots, seems to need a search through up to possibilities.
Compare two puzzles on the same graph. Euler's problem — is there a closed walk using every edge exactly once? — is answered instantly by checking that the graph is connected and every degree is even. Hamilton's problem — is there a cycle through every vertex exactly once? — looks almost the same, yet nobody knows an algorithm that is fundamentally faster than trying exponentially many possibilities.
Computability drew the line between what algorithms can and cannot do in principle. Complexity theory draws a finer line, between what they can do efficiently and what (we believe) they cannot. Its central question was asked by Kurt Gödel in a 1956 letter to John von Neumann, and in its modern form — does P equal NP? — it is one of the seven Millennium Prize Problems. This lesson assumes algorithm design and analysis and basic graph theory.
Polynomial versus exponential
Measure the cost of an algorithm by the number of elementary steps it takes, as a function of the length of its input. An algorithm taking or steps slows down gracefully: doubling the input multiplies the time by or . An algorithm taking steps does not: each extra bit of input doubles the time, and steps would outlast the age of the universe on any conceivable computer.
Polynomial versus exponential growth
Raise k: every polynomial is eventually overtaken by 2^n.
- n^k
- 2^n
- n^k: y = 1
- 2^n: y = 2
Try : the cubic is ahead until about , after which leaves it behind for good. For every there is a crossover, because .
The working hypothesis of the subject — the Cobham–Edmonds thesis — is that "efficiently solvable" means "solvable in polynomial time". Like the Church–Turing thesis it is not a theorem, and it is cruder: an algorithm is useless and a algorithm may be excellent. But polynomial time has two great virtues. It is robust: Turing machines, random-access machines and real programming languages simulate each other with polynomial overhead, so the class of polynomial-time problems does not depend on the model. And it is closed under composition: a polynomial-time algorithm that calls a polynomial-time subroutine is still polynomial-time.
The second idea is the asymmetry of the timetable. For many problems a "yes" answer has a short certificate — a colouring, a cycle, a satisfying assignment — that can be checked quickly, even when finding it seems hard. NP is the class of such problems.
Definitions
A decision problem is a set of finite strings (over , say): the instances whose answer is "yes". Mathematical objects are encoded as strings in any reasonable way — a graph by its adjacency matrix, a number in binary — and is the length of the encoding. (Numbers in unary would be an unreasonable encoding: it is exponentially longer than binary and makes some hard problems look easy.)
Definition— running time, P
A Turing machine (see computability) runs in time if it halts within steps on every input . is the class of problems decided by some machine running in time . The class P is
the problems decidable in polynomial time.
Definition— NP
A problem is in NP if there are a polynomial and a polynomial-time machine , the verifier, such that for every string
The string is a certificate (or witness) for . Equivalently, NP is the class of problems decided in polynomial time by a nondeterministic Turing machine, one that may branch at each step and accepts if some branch accepts — hence the name: Nondeterministic Polynomial time.
Definition— coNP, PSPACE, EXPTIME
coNP is the class of complements of problems in NP: those whose "no" instances have short, efficiently checkable certificates. PSPACE is the class of problems decidable by a machine that uses at most polynomially many tape cells; EXPTIME .
Definition— polynomial-time reduction, NP-complete
reduces to in polynomial time, , if there is a polynomial-time computable function with for all (a Karp reduction). A problem is NP-hard if for every , and NP-complete if moreover .
An NP-complete problem is a hardest problem in NP: a polynomial-time algorithm for it would give one for every problem in NP.
The problems we will use. Formulas are in conjunctive normal form (CNF): an AND of clauses, each an OR of literals or .
- SAT: is a given CNF formula satisfiable? 3-SAT: the same, for formulas with exactly three literals per clause.
- INDEPENDENT SET: given a graph and , does have pairwise non-adjacent vertices?
- VERTEX COVER: given and , is there a set of at most vertices touching every edge?
- CLIQUE: given and , does have pairwise adjacent vertices?
- 3-COL: is a given graph 3-colourable? HAM-CYCLE: does it have a Hamiltonian cycle?
Where problems live
Example— Problems in P
- Reachability: is there a path from to ? Breadth-first search, time linear in the size of the graph.
- 2-colourability: colour greedily along a breadth-first search; the graph is 2-colourable iff no edge joins two vertices of the same colour (iff it has no odd cycle).
- Euler circuits (connected, all degrees even), shortest paths (Dijkstra), minimum spanning trees (Kruskal), 2-SAT (strongly connected components of the implication graph).
- Perfect matching in an arbitrary graph — Edmonds's blossom algorithm (1965), the paper in which he argued that polynomial time is the right notion of a "good" algorithm.
- Linear programming — Khachiyan's ellipsoid method (1979).
- Primality — the AKS algorithm of Agrawal, Kayal and Saxena (2002): deciding whether an -digit number is prime takes time polynomial in , not in the number itself.
Example— Problems in NP, with their certificates
- SAT: a satisfying assignment; the verifier evaluates the formula.
- 3-COL: a colouring; check every edge.
- HAM-CYCLE: the cycle, as a list of vertices; check it visits each vertex once along edges.
- CLIQUE, INDEPENDENT SET, VERTEX COVER: the set of vertices.
- COMPOSITE: given , is it composite? A factor is the certificate; division checks it in polynomial time.
All of these except COMPOSITE are NP-complete. COMPOSITE is in fact in P (by AKS), and the factoring problem in its decision form — does have a factor at most ? — is in (a "no" certificate is the full prime factorisation together with proofs of primality) and is not believed to be NP-complete.
Example— coNP, PSPACE and beyond
- TAUT — is a propositional formula true under every assignment? — is coNP-complete. A "yes" certificate would be a short proof; whether every tautology has a polynomial-size proof in some proof system is exactly the question (Cook and Reckhow, 1979).
- TQBF — is a fully quantified Boolean formula true? — is PSPACE-complete, and so are many two-player games, such as generalised geography (played on a directed graph) and Hex on an board.
- Generalised chess on an board (deciding whether a position is a forced win) is EXPTIME-complete (Fraenkel and Lichtenstein, 1981), so it is provably not in P.
Example— A non-example: a misleading encoding
SUBSET-SUM — given integers and , does some subset sum to ? — is NP-complete. Dynamic programming solves it in about steps. That is not polynomial time: the input length is about bits, and is exponential in . Such "pseudo-polynomial" algorithms are fast only when the numbers are small.
Inclusions, NP-completeness and the hierarchy theorem
The basic inclusions
Theorem— Theorem 1
.
Proof
P ⊆ NP. If a polynomial-time machine decides , take , ignoring the certificate (the empty string will do).
NP ⊆ PSPACE. Let have verifier , running in time , and certificates of length at most . On input , go through all strings of length at most one at a time, in a fixed order, run , and accept if some run accepts. The current takes cells; each run of uses at most cells (a machine cannot visit more cells than it takes steps), and the space is reused for the next . Total space , a polynomial — although the time is exponential.
PSPACE ⊆ EXPTIME. A machine with state set and tape alphabet that uses at most cells has at most distinct configurations (state, head position, tape contents). A deterministic machine that halts never repeats a configuration — otherwise it would cycle forever — so it halts within steps. For polynomial this is .
∎
At least one of these three inclusions is strict, because (Theorem 7 below) — but no one knows which.
Reductions
Theorem— Lemma 2
(a) If and then . (b) If and then . (c) If is NP-complete, and , then is NP-complete.
Proof
(a) Compose the reductions: if runs in time and in time , then has length at most , so is computed in time , a polynomial. (b) Decide by computing and running the polynomial-time algorithm for ; the same estimate applies. (c) Every satisfies , hence by (a).
∎
Part (c) is how NP-completeness spreads: once one NP-complete problem is known, each new one needs only a single reduction. The first one requires a direct argument.
Theorem— Theorem 3 (Cook 1971, Levin 1973)
SAT is NP-complete.
Proof
Sketch. SAT is in NP: the certificate is an assignment. Now let , with verifier running in time and certificates of length . Given of length , we build in polynomial time a CNF formula that is satisfiable iff some certificate makes accept .
The computation of on is a tableau: a grid whose row is the configuration at time (only cells within distance of the start can be visited). Introduce Boolean variables saying "at time , cell holds symbol ", "at time the head is at cell ", "at time the state is " — variables in all, since and are fixed. Add clauses expressing:
- each cell holds exactly one symbol, there is exactly one head position and exactly one state, at each time;
- row is the start configuration with written in place, while the certificate cells are left free — the variables for those cells are the certificate;
- each row follows from the previous one by . The key point is locality: in one step only the cell under the head changes and the head moves one cell, so it suffices to require that every window of the tableau is consistent with — a constant number of clauses of constant size per window;
- the state at time is accepting (after halting, a machine is taken to repeat its final configuration).
There are clauses of bounded size, written down in polynomial time. A satisfying assignment describes an accepting computation of on for the certificate read off row ; conversely an accepting computation gives a satisfying assignment. So .
∎
Theorem— Theorem 4
, so 3-SAT is NP-complete.
Proof
Replace each clause by an equisatisfiable set of 3-literal clauses, using fresh variables for each clause. A clause with becomes
clauses. If some is true, set true for and false for : every new clause is satisfied. If all are false, the chain forces true, then true, …, then true, and the last clause fails. Short clauses are padded: becomes , and becomes four clauses with two fresh variables. The new formula is satisfiable iff the old one is, and it is built in linear time. 3-SAT is in NP, so Lemma 2(c) applies.
∎
Theorem— Theorem 5 (Karp 1972: 3-SAT reduces to INDEPENDENT SET)
Given a 3-CNF formula with clauses , let have a vertex for the -th literal of (), with an edge between two vertices of the same clause, and an edge between any two vertices labelled by complementary literals and . Then is satisfiable if and only if has an independent set of size . Hence INDEPENDENT SET is NP-complete.
Proof
() Fix a satisfying assignment and choose, in each clause, one literal that it makes true. The chosen vertices lie in different clauses, so no triangle edge joins two of them; and two of them cannot be complementary, since both are true. So they form an independent set of size .
() Let be independent with . Each clause's three vertices are pairwise adjacent, so contains at most one of them; since there are clauses and , it contains exactly one vertex from each clause. No two literals in are complementary (they would be adjacent), so we may set every literal in to true consistently, and give the remaining variables any values. Each clause then contains a true literal, so is satisfied.
The graph has vertices and fewer than edges, and is built in polynomial time; INDEPENDENT SET is in NP (the certificate is the set). By Theorem 4 and Lemma 2(c), it is NP-complete.
∎
Theorem— Corollary 6
VERTEX COVER and CLIQUE are NP-complete.
Proof
is independent in iff its complement is a vertex cover: an edge with no endpoint in would have both endpoints in . So reduces INDEPENDENT SET to VERTEX COVER. And is independent in iff it is a clique in the complement graph (same vertices, edges exactly between distinct non-adjacent pairs), so reduces INDEPENDENT SET to CLIQUE. Both problems are in NP.
∎
Thousands of problems have been shown NP-complete in this way: 3-COL (by a gadget reduction from 3-SAT; Garey, Johnson and Stockmeyer, 1976), HAM-CYCLE and the travelling salesperson problem (Karp, 1972), SUBSET-SUM, scheduling, protein folding in simple lattice models, Sudoku and Minesweeper generalised to boards. A remarkable dichotomy is visible already among the examples: 2-SAT and 2-COL are in P, while 3-SAT and 3-COL are NP-complete.
The hierarchy theorem
Theorem— Theorem 7 (time hierarchy; Hartmanis and Stearns 1965, sharpened by Hennie and Stearns 1966)
If is time-constructible and , then . In particular .
Proof
Sketch. ("Time-constructible" means can be computed in time , which holds for all the usual functions.) Diagonalise, as in the halting problem, but with a clock. Let be the machine that, on input , reads as the code of a machine (allowing arbitrary padding), runs a universal simulation of on , stops it after of its own steps, and outputs the opposite of 's answer if the simulation finished (and "no" otherwise). runs in time . Suppose some machine decided the same problem as in time . A universal machine simulates steps of in steps, with a constant depending only on ; since , for long enough padded codes of the simulation of on finishes within steps, and then — a contradiction.
For the corollary: every polynomial is eventually below , so ; and , so .
∎
So EXPTIME-complete problems, like generalised chess, provably have no polynomial-time algorithm. The same argument cannot separate P from NP, as the next section explains.
P versus NP: what is known and what is not
Open. Whether ; whether ; whether . None of the inclusions in Theorem 1 is known to be strict. Most researchers believe — decades of effort have found no polynomial algorithm for any NP-complete problem, and would make finding proofs about as easy as checking them — but belief is not proof.
Known implications. If any NP-complete problem is in P, then (Lemma 2). If then (P is closed under complement). If , then there are problems in NP that are neither in P nor NP-complete (Ladner, 1975); factoring and graph isomorphism are the natural candidates, and Babai (2015) showed graph isomorphism is solvable in quasi-polynomial time .
Why it is hard. Three "barrier" theorems explain why familiar techniques cannot settle the question. Diagonalisation, as in Theorem 7, relativises — it works equally well for machines with access to any oracle — but Baker, Gill and Solovay (1975) built oracles relative to which and others relative to which . Razborov and Rudich (1994) showed that "natural" circuit lower-bound arguments would break widely believed cryptographic assumptions; Aaronson and Wigderson (2008) found a further barrier, algebrisation. The best proven lower bounds for explicit problems in NP are, for general circuits, only linear in .
Cryptography. Public-key cryptography needs functions that are easy to compute and hard to invert. If , no such one-way functions exist, since inverting is an NP search problem. Conversely, alone is not known to be enough: cryptography needs problems that are hard on average, for randomly generated keys, not merely in the worst case. So RSA rests on the unproved assumption that factoring products of two random large primes is hard. The general number field sieve factors in heuristic time , faster than exponential in the number of digits but not polynomial; and Shor's algorithm (1994) factors in polynomial time on a quantum computer, which is why "post-quantum" cryptosystems based on lattice problems are being deployed. See cryptography.
In practice. NP-completeness is a statement about the worst case. Modern SAT solvers routinely decide industrial instances with millions of variables (in hardware and software verification), and SAT solving has settled mathematical questions, such as the Boolean Pythagorean triples problem (Heule, Kullmann and Marek, 2016). Approximation algorithms, parameterised algorithms and heuristics make many NP-hard optimisation problems tractable for the instances that arise.
Satisfiability and reductions in Lean
Lean functions come without a cost model, so this module formalises what complexity theory says about correctness — verifiers, search spaces and reductions — and leaves running times to the prose. (Mathlib does contain a Turing-machine notion of polynomial-time computability, Turing.TM2ComputableInPolyTime; the Cook–Levin theorem has been machine-checked in Coq, by Gäher and Kunze in 2021, and in Isabelle, by Balbach in 2023, but not in Mathlib.)
The verifier. A CNF formula over variables is a list of clauses, each a list of literals; evaluating it under an assignment is the polynomial-time verifier for SAT, and satisfiability is "some certificate passes":
/-- **The verifier for SAT.** A formula is true when every clause is true. Checking a proposed
assignment (a *certificate*) is a single pass over the formula: linear time in its length. -/
def CNF.eval {n : ℕ} (a : Assignment n) (φ : CNF n) : Bool :=
φ.all (Clause.eval a)/-- A formula is satisfiable if some assignment makes it true — "some certificate passes the
verifier". This is the shape of every problem in NP. -/
def Satisfiable {n : ℕ} (φ : CNF n) : Prop :=
∃ a : Assignment n, φ.eval a = trueBrute force runs the verifier on every assignment. The search space has exactly elements, and the algorithm is correct — so SAT is decidable, and the whole question is speed:
/-- **The search space doubles with every variable**: there are exactly `2 ^ n` assignments. -/
theorem length_allAssignments (n : ℕ) : (allAssignments n).length = 2 ^ n := by
induction n with
| zero => rfl
| succ n ih =>
simp only [allAssignments, List.length_flatMap, List.length_cons, List.length_nil]
rw [List.map_const', List.sum_replicate, ih, smul_eq_mul, pow_succ, Nat.mul_comm]/-- The exponential-time algorithm: run the verifier on all `2 ^ n` assignments. -/
def bruteForce {n : ℕ} (φ : CNF n) : Bool :=
(allAssignments n).any fun a => φ.eval a/-- **Brute force is correct**: it answers `true` exactly for the satisfiable formulas.
So SAT is decidable; the open question is whether it can be decided in polynomial time. -/
theorem bruteForce_correct {n : ℕ} (φ : CNF n) : bruteForce φ = true ↔ Satisfiable φ := by
unfold bruteForce Satisfiable
rw [List.any_eq_true]
constructor
· rintro ⟨a, -, ha⟩
exact ⟨a, ha⟩
· rintro ⟨a, ha⟩
exact ⟨a, mem_allAssignments n a, ha⟩Running the algorithm proves that three pigeons do not fit in two holes. The pigeonhole formulas are a classic of proof complexity: Haken (1985) showed that resolution — the proof system underlying most SAT solvers — needs proofs of exponential length for them.
/-- Three pigeons do not fit into two holes: PHP(3, 2) is unsatisfiable. The proof *runs* the
brute-force algorithm (64 assignments, checked by `decide`) and then uses its correctness.
For PHP(n + 1, n) the same method needs `2 ^ (n (n + 1))` assignments, and Haken (1985) proved
that even resolution proofs of these formulas must be exponentially long. -/
theorem php32_unsat : ¬ Satisfiable php32 := by
rw [← bruteForce_correct]
decideReductions. Corollary 6, formalised for Mathlib's SimpleGraph. First the complementation fact, proved here and then quoted from Mathlib:
/-- **Independent sets and vertex covers are complements.** `s` contains no edge if and only if
every edge has an endpoint outside `s`. -/
theorem indepSet_iff_vertexCover_compl {V : Type*} (G : SimpleGraph V) (s : Set V) :
G.IsIndepSet s ↔ G.IsVertexCover sᶜ := by
constructor
· intro hs v w hadj
by_contra h
simp only [Set.mem_compl_iff, not_or, not_not] at h
exact hs h.1 h.2 hadj.ne hadj
· intro hc v hv w hw _ hadj
rcases hc hadj with h | h
· exact h hv
· exact h hw/-- The same fact from Mathlib: `SimpleGraph.isVertexCover_compl`. -/
theorem indepSet_iff_vertexCover_compl' {V : Type*} (G : SimpleGraph V) (s : Set V) :
G.IsIndepSet s ↔ G.IsVertexCover sᶜ :=
SimpleGraph.isVertexCover_compl.symmthen the reduction with its bookkeeping of sizes, and the passage to the complement graph:
/-- **The reduction INDEPENDENT-SET ≤ₚ VERTEX-COVER.** A graph on `n` vertices has an
independent set of size at least `k` if and only if it has a vertex cover of size at most
`n - k`. The reduction maps `(G, k)` to `(G, n - k)` — clearly polynomial time. -/
theorem indep_reduces_to_cover {V : Type*} [Fintype V] [DecidableEq V] (G : SimpleGraph V)
(k : ℕ) (hk : k ≤ Fintype.card V) :
(∃ s : Finset V, k ≤ s.card ∧ G.IsIndepSet (s : Set V)) ↔
(∃ c : Finset V, c.card ≤ Fintype.card V - k ∧ G.IsVertexCover (c : Set V)) := by
constructor
· rintro ⟨s, hks, hs⟩
refine ⟨sᶜ, ?_, ?_⟩
· rw [Finset.card_compl]
omega
· rw [Finset.coe_compl]
exact (indepSet_iff_vertexCover_compl G s).1 hs
· rintro ⟨c, hc, hcov⟩
refine ⟨cᶜ, ?_, ?_⟩
· rw [Finset.card_compl]
omega
· rw [Finset.coe_compl, indepSet_iff_vertexCover_compl, compl_compl]
exact hcov/-- **The reduction CLIQUE ≤ₚ INDEPENDENT-SET**: `s` is a clique in `G` iff it is independent in
the complement graph `Gᶜ`, whose edges join distinct non-adjacent vertices. -/
theorem clique_iff_indepSet_compl {V : Type*} (G : SimpleGraph V) (s : Set V) :
G.IsClique s ↔ Gᶜ.IsIndepSet s := by
constructor
· intro hs v hv w hw hne hadj
exact ((SimpleGraph.compl_adj G v w).1 hadj).2 (hs hv hw hne)
· intro hs v hv w hw hne
by_contra hnot
exact hs hv hw hne ((SimpleGraph.compl_adj G v w).2 ⟨hne, hnot⟩)Finally a certificate of non-colourability, for a graph where a short one exists. For general graphs no short certificates of non-3-colourability are expected: that would put an NP-complete problem in coNP, and so make .
/-- `K₄` is not 3-colourable: a clique of size 4 needs 4 colours. This applies Mathlib's
`SimpleGraph.IsClique.card_le_of_colorable` to the whole vertex set. Deciding 3-colourability
of an arbitrary graph is NP-complete, so no short general argument like this one is expected. -/
theorem k4_not_three_colorable : ¬ (⊤ : SimpleGraph (Fin 4)).Colorable 3 := by
intro h
have hclique :
(⊤ : SimpleGraph (Fin 4)).IsClique ((Finset.univ : Finset (Fin 4)) : Set (Fin 4)) := by
rw [Finset.coe_univ]
exact SimpleGraph.isClique_univ.2 rfl
have h43 := hclique.card_le_of_colorable h
simp at h43What is not formalised here: running times and the classes P and NP themselves, Cook–Levin, the reduction from 3-SAT to INDEPENDENT SET (tested in Python below instead), and the hierarchy theorem.
Testing a reduction, and looking for hard instances
The first program implements Karp's reduction from Theorem 5. It translates certificates in both directions on an example, then decides 200 formulas twice — by trying all assignments, and by a generic branch-and-bound search for an independent set in the reduction graph — and compares the answers.
"""Testing Karp's reduction 3-SAT -> INDEPENDENT SET on many formulas.
The reduction turns a 3-CNF formula with m clauses into a graph: one vertex
for each literal occurrence, a triangle on the three vertices of each clause,
and an edge between every two occurrences of complementary literals x, -x.
The theorem in the lesson says: the formula is satisfiable if and only if
the graph has an independent set of size m.
We test this on 200 small formulas, deciding each side by its own exhaustive
search (all assignments; a generic branch-and-bound search for independent
sets). We also translate certificates both ways on an example. Agreement on
these formulas is evidence that the reduction (and our code) is right; the
proof in the lesson is what shows it for every formula.
"""
import itertools
import random
def random_3cnf(n, m, rng):
"""m random clauses, each on 3 distinct variables of 1..n with random signs."""
formula = []
for _ in range(m):
variables = rng.sample(range(1, n + 1), 3)
formula.append(tuple(v if rng.random() < 0.5 else -v for v in variables))
return formula
def unsat_core(a, b, c):
"""All 8 clauses on the variables a, b, c: unsatisfiable on its own."""
return [(sa * a, sb * b, sc * c)
for sa in (1, -1) for sb in (1, -1) for sc in (1, -1)]
def satisfies(formula, assignment):
"""The verifier: does the assignment (dict var -> bool) satisfy every clause?"""
return all(any((lit > 0) == assignment[abs(lit)] for lit in clause)
for clause in formula)
def satisfying_assignment(formula, n):
"""Exhaustive search over all 2^n assignments; return one or None."""
for bits in itertools.product((False, True), repeat=n):
assignment = {v: bits[v - 1] for v in range(1, n + 1)}
if satisfies(formula, assignment):
return assignment
return None
def reduction_graph(formula):
"""Vertices (clause, position); return (vertices, neighbour bitmasks)."""
vertices = [(i, j) for i in range(len(formula)) for j in range(3)]
masks = [0] * len(vertices)
for (p, u), (q, v) in itertools.combinations(enumerate(vertices), 2):
same_clause = u[0] == v[0]
complementary = formula[u[0]][u[1]] == -formula[v[0]][v[1]]
if same_clause or complementary:
masks[p] |= 1 << q
masks[q] |= 1 << p
return vertices, masks
def _clique_cover_bound(mask, masks):
"""Greedily split `mask` into cliques: an upper bound for independent sets."""
count = 0
while mask:
low = mask & -mask
clique, candidates = low, mask & masks[low.bit_length() - 1]
while candidates:
w = candidates & -candidates
clique |= w
candidates &= masks[w.bit_length() - 1]
mask &= ~clique
count += 1
return count
def independent_set(masks, size):
"""Generic branch and bound: a set of `size` pairwise non-adjacent vertices
(as a sorted list of indices), or None if there is none."""
def search(mask, chosen):
if len(chosen) == size:
return chosen
need = size - len(chosen)
if bin(mask).count("1") < need or _clique_cover_bound(mask, masks) < need:
return None
low = mask & -mask
v = low.bit_length() - 1
found = search(mask & ~low & ~masks[v], chosen + [v])
return found if found is not None else search(mask & ~low, chosen)
return search((1 << len(masks)) - 1, [])
def assignment_to_set(formula, assignment):
"""Satisfying assignment -> independent set: one true literal per clause."""
picks = []
for i, clause in enumerate(formula):
j = next(j for j, lit in enumerate(clause)
if (lit > 0) == assignment[abs(lit)])
picks.append(3 * i + j)
return picks
def set_to_assignment(formula, picks, n):
"""Independent set of size m -> assignment making every picked literal true."""
assignment = {v: False for v in range(1, n + 1)}
for p in picks:
lit = formula[p // 3][p % 3]
assignment[abs(lit)] = lit > 0
return assignment
def is_independent(masks, picks):
return all(not masks[p] >> q & 1 for p, q in itertools.combinations(picks, 2))
def trial_formulas(count=200, n=4, seed=2024):
"""Seeded test formulas; about 40% contain a planted unsatisfiable core."""
rng = random.Random(seed)
formulas = []
for _ in range(count):
m = rng.randint(4, 12)
formula = random_3cnf(n, m, rng)
if rng.random() < 0.4:
core = unsat_core(*rng.sample(range(1, n + 1), 3))
formula = formula[: max(0, m - 8)] + core
rng.shuffle(formula)
formulas.append(formula)
return formulas
def main():
n = 4
example = [(1, 2, -3), (-1, 3, 4), (-2, -4, 3)]
vertices, masks = reduction_graph(example)
edges = sum(bin(x).count("1") for x in masks) // 2
print("Example: (x1 v x2 v -x3) & (-x1 v x3 v x4) & (-x2 v -x4 v x3)")
print(f" graph: {len(vertices)} vertices, {edges} edges "
"(3 triangles + complementary pairs)")
a = satisfying_assignment(example, n)
picks = assignment_to_set(example, a)
print(f" assignment {[int(a[v]) for v in range(1, n + 1)]} -> independent "
f"set {picks}: independent = {is_independent(masks, picks)}")
found = independent_set(masks, len(example))
back = set_to_assignment(example, found, n)
print(f" independent set {found} -> assignment "
f"{[int(back[v]) for v in range(1, n + 1)]}: satisfies = "
f"{satisfies(example, back)}")
print()
rows = []
for formula in trial_formulas():
sat = satisfying_assignment(formula, n) is not None
_, masks = reduction_graph(formula)
indep = independent_set(masks, len(formula)) is not None
rows.append((sat, indep, len(formula)))
agree = sum(s == i for s, i, _ in rows)
print(f"200 formulas on {n} variables, 4 to 12 clauses:")
print(f" satisfiable: {sum(s for s, _, _ in rows)}, "
f"unsatisfiable: {sum(not s for s, _, _ in rows)}")
print(f" 'satisfiable' agrees with 'independent set of size m' in "
f"{agree}/200 cases")
print(f" largest graph: {3 * max(m for _, _, m in rows)} vertices")
print()
print("Epistemic status: experimental evidence. Both sides were decided by "
"exact\nexhaustive search, and they agreed on all 200 formulas tried "
"(not a random\nsample of all formulas: many contain a planted "
"unsatisfiable core). That\nsupports the reduction theorem and our "
"implementation of it; only the proof\nin the lesson covers every "
"formula.")
if __name__ == "__main__":
main()The second program asks where hard instances live. Random 3-CNF formulas with variables and clauses are almost always satisfiable when the density is small and almost never when it is large; the change happens near , a value predicted by statistical physicists (Mézard, Parisi and Zecchina, 2002). A sharp threshold of this kind is a theorem for -SAT with large (Ding, Sly and Sun, 2015) and remains a conjecture for . The search effort of a DPLL solver peaks near the threshold.
"""Random 3-SAT: the satisfiability threshold and where the hard instances are.
We generate random 3-CNF formulas on n = 20 variables with m = r * n clauses
for several clause densities r, and solve each with a small DPLL solver (unit
propagation plus branching). Two things are measured:
* the fraction of satisfiable formulas, which drops from near 1 to near 0
around r = 4.3 (a sharp threshold is conjectured for 3-SAT, near 4.27;
for large k a sharp threshold was proved by Ding, Sly and Sun in 2015);
* the average number of branching nodes, which peaks near the threshold:
formulas far below it are easy to satisfy, far above it easy to refute.
Every "satisfiable" answer comes with an assignment that we re-check with
the verifier; an "unsatisfiable" answer is trusted only because the search
was exhaustive. That asymmetry is the difference between NP and coNP.
"""
import random
def random_3cnf(n, m, rng):
"""m clauses, each on 3 distinct variables of 1..n with random signs."""
formula = []
for _ in range(m):
variables = rng.sample(range(1, n + 1), 3)
formula.append(tuple(v if rng.random() < 0.5 else -v for v in variables))
return formula
def verify(formula, true_literals):
"""The polynomial-time verifier: every clause contains a true literal."""
return all(any(lit in true_literals for lit in clause) for clause in formula)
def dpll(formula, stats):
"""Return a set of true literals satisfying the formula, or None.
stats["nodes"] counts the calls (branching nodes of the search tree).
Variables not mentioned in the returned set may take either value."""
stats["nodes"] += 1
assigned = set()
clauses = [list(c) for c in formula]
while True: # unit propagation
remaining, unit = [], None
for clause in clauses:
if any(lit in assigned for lit in clause):
continue
rest = [lit for lit in clause if -lit not in assigned]
if not rest:
return None
if len(rest) == 1 and unit is None:
unit = rest[0]
remaining.append(rest)
clauses = remaining
if unit is None:
break
assigned.add(unit)
if not clauses:
return assigned
literal = min(clauses, key=len)[0]
for choice in (literal, -literal):
result = dpll(clauses + [[choice]], stats)
if result is not None:
return assigned | result
return None
def complete(true_literals, n):
"""Extend a partial assignment: unmentioned variables become false."""
full = set(true_literals)
for v in range(1, n + 1):
if v not in full and -v not in full:
full.add(-v)
return full
def sweep(n=20, ratios=(2, 3, 4, 4.25, 4.5, 5, 6, 8), trials=30, seed=2024):
"""For each ratio return (ratio, m, fraction satisfiable, mean nodes,
number of certificates that failed verification)."""
rng = random.Random(seed)
rows = []
for r in ratios:
m = round(r * n)
sat = nodes = bad = 0
for _ in range(trials):
formula = random_3cnf(n, m, rng)
stats = {"nodes": 0}
result = dpll(formula, stats)
nodes += stats["nodes"]
if result is not None:
sat += 1
if not verify(formula, complete(result, n)):
bad += 1
rows.append((r, m, sat / trials, nodes / trials, bad))
return rows
def main():
n, trials = 20, 30
print(f"Random 3-SAT, n = {n} variables, {trials} formulas per density r = m/n")
print(" r m satisfiable mean DPLL nodes bad certificates")
rows = sweep(n=n, trials=trials)
for r, m, frac, nodes, bad in rows:
print(f"{r:>5} {m:>5} {frac:>13.2f} {nodes:>17.1f} {bad:>18}")
hardest = max(rows, key=lambda row: row[3])
print(f"Hardest density in this run: r = {hardest[0]}. Brute force would "
f"try 2^{n} = {2 ** n:,}\nassignments for every formula.")
print()
print("Epistemic status: statistical evidence from 240 seeded random "
"formulas at\none small size (n = 20). It illustrates the threshold "
"and the easy-hard-easy\npattern; it does not locate the threshold "
"precisely (finite-size effects\nshift it), and it says nothing about "
"worst-case complexity: P versus NP\nis about all instances, not "
"typical ones.")
if __name__ == "__main__":
main()Notice the column "bad certificates": every "satisfiable" verdict was confirmed by an independent check of the assignment, whereas every "unsatisfiable" verdict is believed only because the search was complete. Certificates exist for one side but — as far as anyone knows — not for the other. That is the difference between NP and coNP, seen in a spreadsheet.
Explore: colouring is easy to check
3-colourability is NP-complete. Colour the graphs by hand, ask for a greedy colouring, and then ask for the fewest colours possible — which the widget finds by exhaustive search, feasible only because the graphs are small.
Find a colouring, check a colouring
Checking a proposed colouring takes one look at each edge. Finding the fewest colours is a search.
0
colours used
0
clashes
10
uncoloured
Give every vertex a colour so that the two ends of each edge differ.
This graph is not planar (it cannot be drawn without edges crossing), so the Four Colour Theorem does not apply to it.
Things to notice: the widget checks your colouring instantly, edge by edge — that is the verifier. Greedy colouring is fast but carries no guarantee: for a bad order of the vertices it can use far more colours than necessary (on the bipartite "crown" graph, minus a perfect matching, it can be forced to use colours), so always compare it with "Fewest possible?". The wheel has an odd rim, so it needs four colours; the Petersen graph, with ten vertices and fifteen edges, needs exactly three.
Exercises
1. Computational complexity
Warm-upHumanWhich of these is the definition of the class NP?
2. Computational complexity
Warm-upHumanWhich of these problems are known to be in P? (Select all that apply.)
3. Computational complexity
Warm-upHumanKarp's reduction from 3-SAT to INDEPENDENT SET is applied to a 3-CNF formula with clauses on variables. How many vertices does the graph have?
4. Computational complexity
CoreHumanIn the reduction from SAT to 3-SAT, a clause with literals is replaced by a chain of 3-literal clauses using fresh variables . How many 3-literal clauses does it become?
5. Computational complexity
CoreHumanYou want to prove that a new problem is NP-complete. Which is enough?
6. Computational complexity
StretchHumanIn the proof of Theorem 5 ( direction) we said: an independent set of size in contains exactly one vertex from each clause. Why?
7. Computational complexity
StretchHumanWhich of these statements are known theorems (not conjectures)? (Select all that apply.)
8. Computational complexity
CoreFormalThe Lean file proves bruteForce_correct : bruteForce φ = true ↔ Satisfiable φ and length_allAssignments : (allAssignments n).length = 2 ^ n. Which conclusion do these two theorems justify?
9. Computational complexity
StretchComputationalImplement Karp's reduction. A 3-CNF formula is a list of clauses, each a tuple of three nonzero integers ( for , for ). Number the vertices for the -th literal () of clause . Write karp_graph(formula) returning the set of edges as pairs (p, q) with p < q: the three edges inside each clause, and an edge between every two vertices whose literals are complementary.
Common misconceptions
Misconception
"NP stands for 'not polynomial'." It stands for nondeterministic polynomial time. Every problem in P is in NP. Whether NP contains problems that are not in P is precisely the open question.
Misconception
"NP-complete problems cannot be solved in practice." NP-completeness says that no polynomial-time algorithm is known that solves every instance. Industrial SAT solvers handle instances with millions of variables, and many NP-hard optimisation problems have excellent approximation algorithms or are easy on the instances that arise.
Misconception
"Polynomial means fast and exponential means slow." Polynomial time is a robust mathematical idealisation. An algorithm is useless; the simplex method takes exponential time on specially built examples (Klee and Minty, 1972) yet is one of the most successful algorithms ever. The theory classifies growth rates, not stopwatch times.
Misconception
"To show a problem is hard, reduce it to SAT." That is backwards. shows that is no harder than SAT (every problem in NP satisfies it). To show is NP-hard, reduce a known NP-hard problem to .
Misconception
"A problem in NP has short certificates for both answers." NP asks for short certificates of "yes" only. Short certificates of "no" define coNP. For SAT, a satisfying assignment certifies "yes", but nobody knows a short certificate that a formula is unsatisfiable.
Misconception
"Quantum computers will solve NP-complete problems efficiently." Shor's algorithm factors integers, a problem not believed to be NP-complete. There is no known efficient quantum algorithm for any NP-complete problem, and Grover's search gives only a square-root speed-up over brute force.
Many ways of seeing
Logic— Complexity classes as logics
Complexity classes can be defined without machines. Fagin's theorem (1974): a property of finite structures is in NP if and only if it is expressible in existential second-order logic — "there exist relations such that a first-order formula holds". For example, 3-colourability is "there exist three sets covering the vertices such that no edge has both ends in the same set". On finite ordered structures, the Immerman–Vardi theorem (1982) characterises P as first-order logic with a least-fixed-point operator. So P versus NP becomes a question about the expressive power of logics. On the proof side, Cook and Reckhow (1979) showed that if and only if some propositional proof system has polynomial-size proofs of all tautologies; Haken's exponential lower bound for resolution on the pigeonhole formulas was a first step in a long programme. See proof theory and first-order logic.
Geometry— Polytopes and linear programming
Optimisation problems are geometry: maximise a linear function over a convex polytope . The simplex method walks along edges of from vertex to vertex; Klee and Minty (1972) deformed the cube so that it visits all vertices, and Spielman and Teng's smoothed analysis (2004) explains why such examples are rare. Khachiyan's ellipsoid method (1979) — shrinking ellipsoids around the feasible region — and Karmarkar's interior-point method (1984) put linear programming in P. For a combinatorial problem one forms the polytope spanned by the solutions: the perfect matchings of a graph span a polytope with an explicit description (Edmonds), whereas for the travelling salesperson problem no tractable description is expected. In fact, Fiorini, Massar, Pokutta, Tiwary and de Wolf (2012) proved unconditionally that the TSP polytope is not the projection of any polytope with polynomially many facets, and Rothvoss (2014) proved the same for the matching polytope — rigorous exponential lower bounds, though for linear-programming formulations, not for all algorithms. See optimisation.
Topology— Certifying knots
Is a given knot diagram a diagram of the unknot? Haken (1961) showed the problem is decidable, using normal surfaces. Hass, Lagarias and Pippenger (1999) showed that it is in NP: if a knot is unknotted, there is a certificate — a normal disc it bounds, encoded compactly — checkable in polynomial time. Lackenby (2021) proved that it is also in coNP: knottedness has short certificates too. So unknot recognition is in , where problems are not expected to be NP-complete — and yet no polynomial-time algorithm is known. Related problems are hard: deciding whether a knot in a 3-manifold bounds a surface of genus at most is NP-complete (Agol, Hass and Thurston, 2006). Low-dimensional topology has become a rich source of natural problems of intermediate or unknown complexity. See knot theory.
Algebra— Determinant versus permanent
The determinant and the permanent differ only in signs. Yet the determinant is computed with arithmetic operations by Gaussian elimination, while computing the permanent of a 0–1 matrix — which counts the perfect matchings of a bipartite graph — is #P-complete (Valiant, 1979), at least as hard as counting the solutions of any NP problem. Valiant's algebraic version of P versus NP, VP versus VNP, asks whether the permanent can be written as the determinant of a polynomially larger matrix of affine functions; it is open. Mulmuley and Sohoni's geometric complexity theory proposes to attack it with algebraic geometry and representation theory of orbit closures — a research programme, not yet a proof. A related algebraic question: the exponent of matrix multiplication satisfies by work up to 2024, and whether is open. See linear algebra.
Probability— Randomness, proofs and approximation
Randomness is a resource. The class BPP contains problems solvable in polynomial time by algorithms that flip coins and err with probability at most ; primality had fast randomised tests (Solovay–Strassen, Miller–Rabin, 1970s) a quarter of a century before AKS made it deterministic, and many believe . The PCP theorem (Arora–Safra; Arora, Lund, Motwani, Sudan and Szegedy, 1992) says every NP statement has a proof that can be checked, with high confidence, by reading only a constant number of randomly chosen bits. Its consequences are sharp: Håstad (2001) proved that, unless , no polynomial-time algorithm can guarantee to satisfy more than a fraction of the clauses of a satisfiable 3-CNF formula — while a uniformly random assignment satisfies of them on average, since each clause fails with probability . And random instances, as in the experiment above, have their own probabilistic theory. See probability theory.
Number theory— Primes are easy, factors seem hard
Deciding primality and finding factors look like twin problems, but they have very different complexity. In 2002 Manindra Agrawal and his students Neeraj Kayal and Nitin Saxena at IIT Kanpur proved that PRIMES is in P, using a polynomial identity: for , is prime if and only if , checked cleverly modulo for a small . Factoring is in but no polynomial-time classical algorithm is known, and the security of RSA (Rivest, Shamir and Adleman, 1977; found earlier, in secret, by Clifford Cocks at GCHQ in 1973) depends on that. The discrete logarithm problem in finite fields and on elliptic curves plays the same role for Diffie–Hellman key exchange. Shor's quantum algorithm solves both, by finding the period of with a quantum Fourier transform. See elementary number theory.
Physics— Spin glasses and the cavity method
Finding the ground state of an Ising spin glass — spins on a lattice minimising with random couplings — is NP-hard for three-dimensional lattices (Barahona, 1982): physics knows NP-hard problems in nature, which is one reason to doubt that physical systems can efficiently solve them. In the other direction, statistical physicists used the non-rigorous cavity method to predict the 3-SAT threshold and a "clustering" of solutions into exponentially many distant groups just below it (Mézard, Parisi and Zecchina, 2002), which inspired the survey-propagation algorithm. Several of these predictions have since been proved for large — an instance where physical heuristics guided rigorous mathematics. Quantum computers define the class BQP; it contains factoring, and is believed not to contain the NP-complete problems.
History and global context
- Before the definitions. Mathematicians have long distinguished methods that "work in principle" from methods that work. In the Soviet Union in the 1950s, Sergey Yablonsky and others studied perebor — brute-force search — and asked whether it could be avoided for problems such as minimising Boolean circuits. On 20 March 1956 Kurt Gödel wrote to John von Neumann asking how fast one could decide whether a mathematical statement has a proof of length , observing that an algorithm running in linear or quadratic time would have consequences of the greatest importance; the letter came to light only in the late 1980s.
- Measuring computation (1960s). Michael Rabin and Dana Scott introduced nondeterminism for finite automata (1959). Juris Hartmanis and Richard Stearns defined time complexity classes and proved the hierarchy theorem (1965). Alan Cobham and Jack Edmonds (1965) independently proposed polynomial time as the mathematical notion of feasibility.
- NP-completeness (1971–73). Stephen Cook, at the University of Toronto, proved that SAT is NP-complete in 1971. Leonid Levin, working in Moscow, independently found the same phenomenon for six "universal search problems" (published 1973); he later emigrated to the United States. Richard Karp (1972) showed that 21 well-known combinatorial problems are NP-complete, revealing how widespread the phenomenon is. Michael Garey and David Johnson's 1979 book catalogued hundreds more.
- Barriers and breakthroughs. Baker, Gill and Solovay found the relativisation barrier (1975); Leonid Khachiyan put linear programming in P (1979); Alexander Razborov proved exponential lower bounds for monotone circuits (1985) and, with Steven Rudich, the natural proofs barrier (1994); the PCP theorem was proved in 1992; Agrawal, Kayal and Saxena put primality in P (2002).
- A prize. In 2000 the Clay Mathematics Institute listed P versus NP among its seven Millennium Prize Problems, with a prize of one million US dollars for a solution.
Further reading
- Sanjeev Arora and Boaz Barak, Computational Complexity: A Modern Approach (2009) — the standard graduate text, from the basics to PCP and beyond.
- Michael Sipser, Introduction to the Theory of Computation (1996; 3rd edition 2012) — Part Three is an excellent first course.
- Michael R. Garey and David S. Johnson, Computers and Intractability: A Guide to the Theory of NP-Completeness (1979) — the classic guide, with a catalogue of NP-complete problems.
- Christos H. Papadimitriou, Computational Complexity (1994).
- Cristopher Moore and Stephan Mertens, The Nature of Computation (2011) — wide-ranging, with physics and random instances.
- Avi Wigderson, Mathematics and Computation (2019) — a survey of the field's connections to the rest of mathematics.
- Lance Fortnow, The Golden Ticket: P, NP, and the Search for the Impossible (2013) — a non-technical introduction.
Next steps in the Lumenary: proof theory (proof complexity), cryptography, and graph theory, the home of many NP-complete problems.