Part IV · trunk C · constraint programming and SAT · spine
Unit propagation, conflict analysis, backjumping, activity, restarts — resolution with a very good heuristic, and a proof system with limits you can state precisely.
needs 17~2 h slides + 3½ h lablab: uv run co lab 20
20 · overview
CNF, resolution, unit propagation, watched literals.
Implication graphs, first UIP, backjumping, VSIDS, restarts, LBD; why it stops.
DRAT; CDCL is resolution; pigeonhole's exponential lower bound.
Tseitin, at-most-one, cardinality networks: why the smallest CNF is not the best.
Words with a dotted underline are glossary terms: click one for its definition. G lists every term in the unit, N shows the notes. Optional extra: interactive figures, linked from the slides they support by a small ▸ chip; skip them freely.
section 1
Branch, propagate, and remember why you failed.
20 · DPLL
CNF: a conjunction of clauses, each a disjunction of literals. SAT is NP-complete (Cook 1971); 3-SAT already is.
Sound and complete for refutation: $F$ is unsatisfiable iff the empty clause can be derived.
DPLL (Davis, Logemann & Loveland 1962): branch on a variable, apply unit propagation (a clause with all literals false but one forces it), and backtrack chronologically on a conflict.
Watch two non-false literals per clause. A clause needs attention only when a watched literal becomes false: find a replacement, or the clause is unit or conflicting.
Backtracking costs nothing: unassigning variables never invalidates a watch. No counters to restore.
Lab step 1's test: on 40 random formulas with random decisions, the watch loop reaches exactly the assignments and conflicts of naive propagation.
20 · DPLL · example
$c_1 = (\lnot x_1 \lor x_3)$, $\ c_2 = (\lnot x_2 \lor \lnot x_3 \lor x_4)$, $\ c_3 = (\lnot x_2 \lor \lnot x_4 \lor x_5)$, $\ c_4 = (\lnot x_4 \lor \lnot x_5)$. Each clause watches its first two literals. Decide $x_1$, then $x_2$.
| just assigned | visit clauses watching | what happens |
|---|---|---|
| $x_1$ (decision, level 1) | $\lnot x_1$: $c_1$ | no replacement, other watch $x_3$ free: unit, $x_3$ |
| $x_3$ | $\lnot x_3$: $c_2$ | replacement $x_4$ is free: watch moves to $x_4$ |
| $x_2$ (decision, level 2) | $\lnot x_2$: $c_2$, $c_3$ | $c_2$: only $\lnot x_3$ left, false: unit, $x_4$. $c_3$: watch moves to $x_5$ |
| $x_4$ | $\lnot x_4$: $c_3$, $c_4$ | $c_3$: unit, $x_5$. $c_4$: both literals false: conflict |
$x_1$ touched one clause, $x_3$ one. $c_4$ was not looked at until $x_4$ was set: before that it had two unassigned watches, so it could not be unit.
Undoing level 2 unassigns $x_2, x_4, x_5$ and moves no watch back: $c_2$ still watches $x_4$ and $\lnot x_2$, both unassigned again, which is a valid state.
section 2
A conflict is a lesson; write it down.
20 · learning
Lab step 2's textbook example. Clauses (¬x₁∨x₃), (¬x₂∨¬x₃∨x₄), (¬x₂∨¬x₄∨x₅), (¬x₄∨¬x₅). Decisions x₁, then x₂.
Implication graph: a node per assigned literal (at its decision level, on the trail), with edges from the reason clause's other literals. A conflict is a clause with every literal false.
A UIP is a current-level node on every path from the decision to the conflict. The first UIP is the one closest to the conflict.
Here x₄ is not a UIP: x₂ → x₅ bypasses it. The first UIP is x₂, and the learned clause is ¬x₂ ∨ ¬x₃: backjump to level 1 and assert ¬x₂. The next slide derives it.
Computing it is resolution: resolve the conflict clause with the reason of the latest current-level literal until one current-level literal remains.
20 · learning · example
| clause so far | level-2 literals | resolve with |
|---|---|---|
| $c_4 = (\lnot x_4 \lor \lnot x_5)$ | 2 | reason of $x_5$: $c_3$ |
| $(\lnot x_2 \lor \lnot x_4)$ | 2 | reason of $x_4$: $c_2$ |
| $(\lnot x_2 \lor \lnot x_3)$ | 1 | stop |
One level-2 literal left, $\lnot x_2$: the first UIP. The clause's other literal, $\lnot x_3$, is from level 1, so backjump to level 1. There the clause is unit and asserts $\lnot x_2$ (an asserting clause). Its LBD is 2: levels 1 and 2.
To accept $(\lnot x_2 \lor \lnot x_3)$ as a lemma, assume its negation, $x_2$ and $x_3$, and propagate over the clauses so far:
$c_2$ gives $x_4$. $c_3$ gives $x_5$. $c_4$ is now false: conflict, so the lemma is implied. ✓
No search, no heuristics: two unit propagations. That is why checking a DRAT proof is cheap even when finding it wasn't.
The backjump skipped nothing here, but with ten decisions between level 1 and the conflict it would skip all of them: non-chronological backjumping.
20 · learning
Literal block distance: the number of distinct decision levels in a learned clause. "Glue" clauses (LBD ≤ 2) link two blocks of propagation and are kept forever. Periodically delete the worse half of the rest, never a clause that is a current reason.
Lab then, random 3-SAT at the threshold:
| n | your conflicts | MiniSat | your s | MiniSat s |
|---|---|---|---|---|
| 150 unsat | 2 236 | 3 051 | 0.40 | 0.015 |
| 200 sat | 8 588 | 7 748 | 2.31 | 0.048 |
The same conflicts, 30–50× the time per conflict: the algorithm is right, the language is slow.
20 · learning · termination
It is not obvious. VSIDS may branch on any variable, and a backjump can throw away every level, so the number of free variables goes back up. Counted level by level, it does go down.
For each decision level $\ell = 0, \dots, n$, let $f_\ell$ be the number of variables not assigned at any level $\le \ell$ on the trail. Read them as one number in base $n+1$, level 0 leading:
$$\mu = \sum_{\ell=0}^{n} f_\ell \,(n+1)^{\,n-\ell}$$A level not yet open adds nothing, so its digit repeats the one before.
Each step lowers the first digit it changes. $\mu \ge 0$ and starts below $(n+1)^{n+1}$, so the run ends.
full proof in the notes (N)
$\mu$ counts free variables per level, never which ones. VSIDS and phase saving pick which literal joins a level, not how many do. The heuristic owes one thing: an unassigned variable.
20 · learning · termination · example
| step | free after levels 0–5 | $\mu$ |
|---|---|---|
| start | 5 5 5 5 5 5 | 46 655 |
| decide $x_1$ | 5 4 4 4 4 4 | 45 100 |
| propagate $x_3$ | 5 3 3 3 3 3 | 43 545 |
| decide $x_2$ | 5 3 2 2 2 2 | 43 286 |
| propagate $x_4$, $x_5$ | 5 3 0 0 0 0 | 42 768 |
| conflict, jump to 1, $\lnot x_2$ | 5 2 2 2 2 2 | 41 990 |
| decide $x_4$ | 5 2 1 1 1 1 | 41 731 |
| propagate $\lnot x_5$: all set | 5 2 0 0 0 0 | 41 472 |
Down on every row, including the jump: digits 2 to 5 go back up from 0 to 2, but digit 1 falls from 3 to 2.
The jump of the implication graph slide. Under each level, the variables still free after it. Digit 1 falls from 3 to 2 and outweighs digit 2's rise: one unit of digit 1 is worth $6^4 = 1\,296$, more than all later digits can hold ($1\,295$). Decisions outlined; the shaded literal is the one the learned clause asserts; dashed cells are unassigned again.
20 · learning · termination
The example with $c_2$ shortened to $(\lnot x_2 \lor x_4)$. Same decisions, same conflict, but analysis ends at the unit learned clause $(\lnot x_2)$: no level-1 literal is involved, so the jump goes to level 0 and undoes $x_1$ and $x_3$ too. Shading as on the previous slide.
Five literals undone, and digits 1 to 5 jump back up to 4. But the leading digit, the variables not fixed at level 0, falls from 5 to 4: $42\,768 \to 37\,324$.
Level 0 is never undone, so that digit never rises, and at most $n$ jumps land there. Between two of them, the digit for level 1 only falls, and so on down.
A restart cancels every level above 0, sending those digits back up. But a run without restarts ends within $(n+1)^{n+1}$ steps, and Luby cutoffs grow without bound: some run is given that many conflicts, and ends.
Or keep every learned clause: each one is new, and there are only $3^n$ clauses. The lab's solver deletes clauses, so it relies on Luby.
section 3
"Unsatisfiable" should come with evidence.
20 · proofs
A DRAT proof lists every learned clause (and every deletion) in order. A checker replays it: each lemma must be RUP, meaning that assigning its negation and unit-propagating over the clauses so far gives a conflict. It ends with the empty clause.
Lab then, proofs from your solver, checked by colib.drat (an independent RUP checker):
| instance | lemmas | check |
|---|---|---|
| 3-SAT n = 120, unsat | 831 | accepted, 0.2 s |
| pigeonhole 7 into 6 | 784 | accepted, 0.2 s |
The tests also check 30 proofs, with aggressive deletions included. A solver bug that learns a wrong clause fails the check, even if the final answer happens to be right.
20 · limits
CDCL with restarts polynomially simulates general resolution, and every CDCL run yields a resolution refutation of similar size. So a resolution lower bound is a CDCL lower bound.
Every resolution refutation of the pigeonhole formula $\mathrm{PHP}^{n+1}_n$ has size $2^{\Omega(n)}$.
Why, in one line: any refutation must at some point hold a clause about a third of all the pigeons at once, and building clauses that wide one resolution at a time takes exponentially many steps. sketch in the notes (N)
Counting ("$n+1$ objects don't fit in $n$ places") can't be expressed compactly in clause-by-clause reasoning. Unit 18's alldifferent does it in one matching.
Lab then, conflicts:
| pigeonhole | yours | MiniSat | CaDiCaL | Kissat s |
|---|---|---|---|---|
| 7 into 6 | 784 | 2 498 | 1 044 | 0.004 |
| 9 into 8 | 41 364 | 39 063 | 31 719 | 0.13 |
| 10 into 9 | — | 510 308 | 267 023 | 0.50 |
| 11 into 10 | — | 10.7 M | 2.2 M | 3.2 |
Better engineering shifts the curve; it doesn't change its shape. Stronger proof systems (cutting planes, extended resolution) have short pigeonhole proofs, and pseudo-Boolean solvers exploit that.
section 4
The same constraint, very different CNFs.
20 · encodings
Arc consistency is the property that matters: does unit propagation on the encoding prune as much as the constraint would? Pairwise and the sequential counter both have it.
Lab then, n-queens with your solver (clauses / seconds):
| n | pairwise | sequential | bitwise |
|---|---|---|---|
| 40 | 103 520 / 0.33 | 18 292 / 3.82 | 35 584 / 0.88 |
| 60 | 352 880 / 0.97 | 41 832 / 20.9 | 83 604 / 10.5 |
The largest CNF is the fastest. Pairwise propagates in one step. The counter needs chains of auxiliary variables, which also distract the branching heuristic. Fewer clauses is not the objective; propagation strength and a branching-friendly variable set are.
20 · encodings · example
Circuit $o = (a \land b) \lor c$, asserted true. Name the gates $g = a \land b$ and $o = g \lor c$:
| gate | clauses |
|---|---|
| $g \leftrightarrow a \land b$ | $(\lnot g \lor a)$, $(\lnot g \lor b)$, $(g \lor \lnot a \lor \lnot b)$ |
| $o \leftrightarrow g \lor c$ | $(\lnot o \lor g \lor c)$, $(o \lor \lnot g)$, $(o \lor \lnot c)$ |
| output | $(o)$ |
7 clauses. The circuit has 5 satisfying inputs $(a,b,c)$, and each extends to exactly one value of $g, o$: same count, so equisatisfiable, and models map back.
Pairwise, 6 clauses: $(\lnot x_1 \lor \lnot x_2)$, $(\lnot x_1 \lor \lnot x_3)$, $(\lnot x_1 \lor \lnot x_4)$, $(\lnot x_2 \lor \lnot x_3)$, $(\lnot x_2 \lor \lnot x_4)$, $(\lnot x_3 \lor \lnot x_4)$.
Sequential counter, 8 clauses and 3 new variables $s_1, s_2, s_3$ ("one of the first $i$ is true"): $(\lnot x_1 \lor s_1)$, $(\lnot x_2 \lor s_2)$, $(\lnot s_1 \lor s_2)$, $(\lnot x_2 \lor \lnot s_1)$, $(\lnot x_3 \lor s_3)$, $(\lnot s_2 \lor s_3)$, $(\lnot x_3 \lor \lnot s_2)$, $(\lnot x_4 \lor \lnot s_3)$.
Set $x_1$ true. Pairwise: one round gives $\lnot x_2, \lnot x_3, \lnot x_4$. Counter: $s_1$, then $\lnot x_2$ and $s_2$, then $\lnot x_3$ and $s_3$, then $\lnot x_4$. Same result, after a chain of six propagations.
20 · lab
uv run co test 20
uv run co then 20 # MiniSat, CaDiCaL, Kissat · pigeonhole · encodings · DRAT checks
20 · checkpoint
Marques-Silva, Lynce & Malik, Handbook of Satisfiability, ch. 4.
Eén & Sörensson, "An extensible SAT-solver" (2003).
Audemard & Simon, IJCAI 2009 (LBD).
Haken, TCS 39 (1985); Buss's survey (2012).
21 lazy clause generation: CP propagators that explain themselves to this solver · 22 hardness and reductions.