Why SAT Solving Matters
A feature configuration might require that enabling an extension also enables its runtime, that two drivers cannot be installed together, and that at least one storage backend is selected. Each individual rule is simple; deciding whether all the rules can hold together can require many interacting choices. Boolean satisfiability, or SAT, asks whether there is any assignment of true and false values that satisfies every rule.
SAT also provides a useful way to search for mistakes. Encode a circuit or a bounded program execution together with the condition “the expected property fails.” A satisfying assignment describes a counterexample within that encoding. An UNSAT result means no such assignment exists for the encoded instance; a finite execution bound does not establish correctness for every possible execution length.
This lesson extends recursive constraint search to propositional formulas. First build DPLL, with unit propagation, pure-literal choices, and chronological backtracking. Then retain explanations for implications and turn a conflict into a learned clause: CDCL, with first-UIP analysis, nonchronological backjumping, a simplified VSIDS activity rule, and geometric restarts. Both executable modes share a two-watched-literal propagation engine, so the connection between logical reasoning and state management remains visible.
SAT finds one Boolean assignment satisfying every clause, or establishes that none exists. DPLL combines case splitting with forced consequences and undo; CDCL additionally derives clauses from conflicts so later search can avoid the same incompatible combination of assignments.
Core Concepts and Terminology
Let $x_1,\ldots,x_n$ be Boolean variables. A literal is $x_i$ or its negation $\neg x_i$. A clause is a disjunction of literals, and a formula in conjunctive normal form, or CNF, is a conjunction of clauses:
$$F=\bigwedge_{j=0}^{m-1}C_j,\qquad C_j=\bigvee_{\ell\in C_j}\ell.$$
Every clause must have at least one true literal. An unassigned variable is distinct from false. Under a partial assignment, a clause is satisfied if some literal is true, conflicting if every literal is false, unit if exactly one literal is unassigned and all others are false, and otherwise unresolved. A branch conflict disproves that partial assignment; it does not by itself prove that the whole formula is UNSAT.
The programs encode $x_i$ by integer $i$ and $\neg x_i$ by $-i$. For example, [-1,2] represents $\neg x_1\lor x_2$. Internally, assignment values are $1$ for true, $-1$ for false, and $0$ for unassigned; returned models use ordinary bits, $1$ for true and $0$ for false. Literal zero is invalid in this API. This is a list-of-clauses interface, rather than a DIMACS parser with zero-terminated lines.
Two boundary cases matter: the empty conjunction [] is true, while a formula containing the empty clause, such as [[]], is false. A clause containing both $x_i$ and $\neg x_i$ is a tautology and can be discarded. Repeating the same literal does not create a second logical alternative: [1,1] is a unit clause after duplicate removal.
A trail records assigned literals in order. Decisions start new decision levels; unit implications inherit the current level and record the clause that forced them. Level zero contains assignments made before any decision. A saved trail boundary lets the solver undo a whole level, including its implications.
The tree uses the six-clause worked formula introduced below. The true branch of $x_1$ forces $x_2$ and immediately conflicts; the false branch is satisfiable. Both branches begin from the same root assignment, so an implication from the failed branch must not leak into its sibling.
| Term | Definition | Example or operational role |
|---|---|---|
| SAT / model | At least one complete assignment satisfies every clause | A model is a witness that can be evaluated directly |
| UNSAT | No assignment satisfies the formula | Requires exhaustive sound search or a sound refutation, not one failed branch |
| Unit propagation / BCP | Assign the sole remaining nonfalse literal of a unit clause | With $x_1$ true, $\neg x_1\lor x_2$ forces $x_2$ |
| Pure literal | An unassigned variable has only one polarity in unsatisfied clauses | Choosing that polarity preserves satisfiability but may discard other models |
| Watched literals | Two positions used to detect when a clause needs attention | A falsified watch triggers a replacement search or a unit/conflict check |
| Decision level and reason | A decision's depth, and the clause explaining an implication | reason[2]=0 means clause 0 forced the assigned value of variable 2 |
| DPLL | Propagation plus branching and chronological backtracking | On failure, undo the newest branch and try its opposite |
| CDCL | Propagation plus conflict analysis, learning, and backjumping | Retain a derived clause across later branches and restarts |
| First UIP | The first unique implication point encountered by reverse conflict analysis | Stop resolving when one learned-clause literal belongs to the current level |
| VSIDS | Variable State Independent Decaying Sum | Favor unassigned variables associated with recent conflicts |
| Restart | Return to level zero while retaining useful search information | Keep learned clauses and activity scores; remove nonroot assignments |
How SAT Solving Works, Step by Step
Begin by normalizing the clauses, attaching their watches, and enqueueing every original unit clause with its clause ID as the reason. An empty clause or incompatible original units establishes UNSAT immediately. Otherwise, process the trail as a queue until propagation reaches a fixed point or finds a conflict.
Two-watched-literal propagation. For each nonunit clause, store two distinct positions and register the clause in the watch list of each watched literal. A unit clause has both stored positions equal to zero but is registered only once. In these implementations the lists are indexed by the literal actually watched. When assigned literal $p$ is dequeued, inspect the list for $\neg p$, because that literal just became false.
For each affected clause:
- If its other watched literal is true, leave both watches where they are. The clause is satisfied even though one watch is false.
- Otherwise, look for a nonwatched literal that is not false: true or unassigned are both valid replacements. Move the false watch there, remove the clause from the old bucket by swapping in the last entry, and append it to the new bucket. Reprocess the same bucket index after removal so the swapped-in entry is not skipped.
- If no replacement exists and the other watch is unassigned, enqueue that other literal with this clause as its reason.
- If no replacement exists and the other watch is false, return this clause ID as a conflict.
Moving watches changes an index structure, not the logical formula. Watch positions survive backtracking. The assignment trail, levels, reasons, and propagation head must be rolled back together.
DPLL mode. After each propagation fixed point, scan only unsatisfied clauses for pure unassigned variables. Choose the lowest-ID pure variable, assign its satisfying polarity, and propagate again. Once no pure literal remains, return SAT if all clauses are satisfied. Otherwise choose the lowest-ID unassigned variable, try true first, and recurse. Save a trail boundary before the decision; on return, undo the branch even if it succeeded. If the true branch fails, try false. If both fail, return failure to the caller. The satisfying model is copied before successful assignments are undone.
Unit propagation is a logical consequence of the formula and the current trail. A pure-literal assignment is a safe choice, rather than necessarily a logical consequence. For $x_1\lor x_2$, choosing pure $x_1$ true preserves the existence of a solution, but the model with $x_1$ false and $x_2$ true also existed. Therefore this rule needs different treatment in model counting.
CDCL mode. Omit pure-literal choices from this engine so every nondecision assignment has a clause reason. On a nonroot conflict, start with the falsified clause. While it contains more than one literal assigned at the current decision level, take its latest such variable on the trail and resolve with that variable's reason clause. Stop when exactly one current-level literal remains. This is a first-UIP learned clause. Keep that literal first, and place a literal from the greatest remaining level second, if one exists.
The backjump target is the highest level of the learned clause's other literals, or zero for a unit learned clause. Undo every higher level, attach the learned clause, and enqueue its first literal with the new clause ID as the reason. Its other literals are still false, so the clause is asserting. The implementation keeps level-zero literals in resolvents rather than silently dropping them.
The CDCL branching score is a transparent VSIDS-style variant, initialized to zero. At each analyzed conflict, halve every variable activity and add one to each variable appearing in the starting conflict or a reason used in resolution:
$$a_v\leftarrow \tfrac12a_v+\begin{cases}1,&v\text{ participates in the analysis},\\0,&\text{otherwise}.\end{cases}$$
Choose the unassigned variable with greatest activity, breaking ties by lowest ID, and choose its true polarity. The code scans scores directly; it has no heap, phase saving, or learned-clause deletion. This is a teaching implementation of decaying conflict activity, not a reproduction of one particular solver's tuning.
After a conflict-free propagation pass, check SAT first. If clauses remain unresolved and the restart conflict budget has been reached at a nonzero level, backtrack to zero while preserving learned clauses and scores. Reset the conflicts-since-restart count and double the next budget. The sample uses budgets $1,2,4,\ldots$ so the effect is visible; the default Python/C++ argument is two. The flowchart shows this CDCL loop; DPLL's alternative continuation is the two-branch recursion described above.
flowchart TD
A["Normalize clauses; attach watches; enqueue root units"] --> B["Propagate queued assignments"]
B --> C{"Conflicting clause?"}
C -->|Yes| D{"Decision level zero?"}
D -->|Yes| E["Return UNSAT; clear assignment state"]
D -->|No| F["Resolve to first UIP; update activities"]
F --> G["Backjump; attach learned clause; assert its first literal"]
G --> B
C -->|No| H{"Every clause satisfied?"}
H -->|Yes| I["Copy model; return SAT; clear assignment state"]
H -->|No| J{"Restart due at nonzero level?"}
J -->|Yes| K["Backtrack to zero; retain learning; double budget"]
K --> B
J -->|No| L["Choose highest-activity unassigned variable"]
L --> M["Start decision level; enqueue positive literal"]
M --> B
Worked Example with a Full Trace
Use four variables and these six clauses, preserving the listed literal order and clause IDs:
| Clause ID | Integer form | Logical form |
|---|---|---|
| 0 | [-1,2] | $\neg x_1\lor x_2$ |
| 1 | [-1,-2] | $\neg x_1\lor\neg x_2$ |
| 2 | [1,-2] | $x_1\lor\neg x_2$ |
| 3 | [1,3] | $x_1\lor x_3$ |
| 4 | [-3,4] | $\neg x_3\lor x_4$ |
| 5 | [-4,3] | $\neg x_4\lor x_3$ |
Every variable initially occurs in both polarities, and there are no units. Every clause is binary, so its two watches remain on its two literal positions throughout this run. Watch movement is illustrated separately by the watch move sample.
DPLL. Entry 1 has trail [], level zero, and queue head zero. Decide literal 1 at level one, saving boundary zero. Entry 2 dequeues 1, so it scans clauses watching -1. Clause 0 enqueues 2, with reason 0. Clause 1 now has both literals false and returns conflict before queued 2 is processed. The trail is [1,2] and head is one.
Undo to the saved boundary: pop 2, then 1; clear both reasons and levels and reset head to zero. Decide -1 at a fresh level one. Entry 3 processes that decision and the implications as follows. The implication total includes 2 from the failed branch and never decreases during undo.
| Event in successful DPLL branch | Trail after event | Assignment level / reason | Cumulative implications |
|---|---|---|---|
Decide -1 | [-1] | Level 1; no reason clause | 1 |
Process -1; clause 2 forces -2 | [-1,-2] | Level 1; reason 2 | 2 |
Still processing -1; clause 3 forces 3 | [-1,-2,3] | Level 1; reason 3 | 3 |
Process -2 | [-1,-2,3] | Clause 0 is already true through -1 | 3 |
Process 3; clause 4 forces 4 | [-1,-2,3,4] | Level 1; reason 4 | 4 |
Process 4 | [-1,-2,3,4] | Clause 5 is true through 3; head reaches 4 | 4 |
All clauses are satisfied. Copy model $[0,0,1,1]$, undo the successful level, and return SAT. Totals: three recursive entries, two decision assignments, four implications, no pure choices, and one conflict.
CDCL. Entries 1 and 2 make the same initial decision and encounter the same conflict. Resolve clause 1 with the reason for 2, clause 0:
$$\frac{(\neg x_1\lor\neg x_2)\qquad(\neg x_1\lor x_2)}{\neg x_1}.$$
The learned clause [-1] has one current-level literal, so it is a first-UIP clause and its backjump target is zero. Give variables 1 and 2 activity one, undo [1,2], attach learned clause 6, and enqueue -1 at level zero with reason 6. Entry 3 then enqueues -2, 3, and 4 for reasons 2, 3, and 4. The same model satisfies all clauses. CDCL uses one decision and five implications: the failed-branch 2, the learned assertion -1, and the three final implications. It learns one clause and performs no restart because it reaches SAT first.
A second small example makes backjumping and restarting visible. The restart fixture has clauses [-1,-3,4], [-1,-3,-4], [3,5,6], with six variables and restart budget one. With initially tied scores, CDCL decides 1, then the irrelevant variable 2, then 3, at levels 1, 2, and 3. The first clause forces 4; the second conflicts. Resolving on $x_4$ learns $\neg x_3\lor\neg x_1$, stored as [-3,-1]. Backjump to level 1, skipping level 2, and assert -3 there. The remaining clause needs $x_5\lor x_6$, so the formula is still unresolved after propagation.
The budget now triggers a restart to zero. Keep [-3,-1] and the activities of variables 1, 3, and 4. Decide 1 again, which propagates -3 through the learned clause. Decisions 4, 2, and 5 then complete a model. This run has ten loop entries, seven decisions, three implications, one learned clause, and one restart. The irrelevant variable can still be selected by the simple scan when its activity ties other remaining variables; the code has no clause-relevance filter for branching.
Trace It by Hand
Pause at the main example's conflict with trail [1,2], levels [1,1], reason for 2 equal to clause 0, and conflict clause 1. Predict the learned clause, the backjump target, and the next four literals on the trail.
Answer: resolve [-1,-2] with [-1,2] on variable 2, obtaining [-1]. Jump to zero and assert -1 with learned clause 6 as the reason. Clauses 2, 3, and 4 then produce the trail [-1,-2,3,4], all at level zero.
For the restart fixture, predict why the jump target is 1 rather than 2 or 0. Answer: the learned clause [-3,-1] has its asserting literal at level 3 and its only other literal at level 1. The decision on variable 2 did not participate in the conflict, so it is removed without trying its opposite. The later restart is a separate policy decision and retains the learned clause.
Implementation in Python, C++, and Java
Each complete program contains both engines, with identical normalization, clause order, watch-bucket mutation, positive decision polarity, tie breaking, activity updates, and restart policy. Python names use underscores and Java uses its usual method naming; the logical operations and printed results match.
Inputs are non-null sequences of non-null clauses with integer literals. The demonstration accepts at most 16 variables, 256 raw clauses, and 4,096 raw literal occurrences. Validate every literal, including literals in a clause that will eventually be discarded as tautological. Duplicate literals are removed in their first-occurrence order; tautological clauses are dropped. Input data is copied and never changed. Empty clauses remain available to the initial contradiction check.
DPLL scans unresolved clauses for pure literals after every propagation fixed point. CDCL uses first-UIP resolution with simple lists, an activity scan, and a geometric restart budget. The sample explicitly passes budget one. C++ and Java use 64-bit instrumentation counters. These limits keep the examples inspectable; they are not a promise of quick solving for every accepted input. The implementation retains all learned clauses, so memory can grow substantially on harder formulas.
Both modes return a SAT flag, a copied total model on success, and seven counters. When all clauses are already satisfied under a partial assignment, unused variables are completed as false. UNSAT returns an empty model. For zero variables, an empty model can also be a valid SAT witness, so always consult the flag. Calling solve again rebuilds the working clause database, watches, activities, and counters from the normalized original formula.
Counter definitions are precise:
nodes: DPLL recursive entries, or CDCL loop entries before propagation; these are different control-flow units, not a common search-tree size.decisions: explicit decision assignments, including the opposite branch in DPLL and repeated choices after a restart.implied: newly enqueued assignments with a clause reason, including original units and learned assertions; repeated implication events are counted again.pure: DPLL pure-literal choices, separate from implications and decisions.conflicts: detected propagation conflicts; an initial empty clause or incompatible original units counts as one initial contradiction.learned: clauses added by nonroot conflict analysis.restarts: actual policy backtracks to zero; ordinary backjumps are separate.
Before returning, the code clears assignments, levels, reasons, the trail, decision boundaries, and the queue head. restored=YES refers to that assignment state. Watch movement is deliberately preserved during branch undo, and learned clauses deliberately survive CDCL restarts. Neither is required to match the initial arrays. The next public solve rebuilds them.
Expected stdout, identical in all three languages:
backtrack / DPLL: SAT, nodes=3, decisions=2, implied=4, pure=0, conflicts=1, learned=0, restarts=0model=[0,0,1,1], restored=YESbacktrack / CDCL: SAT, nodes=3, decisions=1, implied=5, pure=0, conflicts=1, learned=1, restarts=0model=[0,0,1,1], restored=YESwatch move / DPLL: SAT, nodes=1, decisions=0, implied=4, pure=0, conflicts=0, learned=0, restarts=0model=[1,1,0,1], restored=YESwatch move / CDCL: SAT, nodes=1, decisions=0, implied=4, pure=0, conflicts=0, learned=0, restarts=0model=[1,1,0,1], restored=YESpure / DPLL: SAT, nodes=1, decisions=0, implied=0, pure=2, conflicts=0, learned=0, restarts=0model=[1,0,0], restored=YESpure / CDCL: SAT, nodes=3, decisions=2, implied=1, pure=0, conflicts=0, learned=0, restarts=0model=[1,1,1], restored=YESunsat / DPLL: UNSAT, nodes=3, decisions=2, implied=2, pure=0, conflicts=2, learned=0, restarts=0model=[], restored=YESunsat / CDCL: UNSAT, nodes=3, decisions=1, implied=3, pure=0, conflicts=2, learned=1, restarts=0model=[], restored=YESrestart / DPLL: SAT, nodes=1, decisions=0, implied=0, pure=2, conflicts=0, learned=0, restarts=0model=[0,0,1,0,0,0], restored=YESrestart / CDCL: SAT, nodes=10, decisions=7, implied=3, pure=0, conflicts=1, learned=1, restarts=1model=[1,1,0,1,1,0], restored=YESchecks = PASS
The pure fixture illustrates why different valid models across solver modes are expected: DPLL makes two pure choices, whereas CDCL makes decisions and a unit implication. Neither mode counts models. The unsat fixture contains all four binary sign combinations on two variables; each assignment falsifies one of its clauses.
Validation used 8,433 inputs per language in both modes, giving 16,866 mode/input cases per language. The corpus includes every subset of the nine canonical clauses over two variables; every family of at most four canonical clauses of width at most two over three variables; 2,800 seeded random formulas; fifty 16-variable random 3-CNF formulas; pigeonhole instances under multiple restart budgets; boundary fixtures; and ten invalid inputs. The valid corpus has 4,577 SAT and 3,846 UNSAT formulas. Exhaustive truth tables independently checked each answer and each returned model. Both modes were repeated on the same solver object, with identical results and unchanged inputs.
Instrumented Python additionally performed 85,566 implication-reason checks, 39,834 watch-list membership checks, 33,988 successful propagation fixed-point checks, 38,800 undo checks, and 1,332 learned-clause checks. Each learned clause had exactly one current-level literal, was false at the conflict assignment, and held in every original satisfying assignment found by the independent oracle. Across one run per mode/input, CDCL learned 666 clauses and executed 92 restarts. C++ ran with address and undefined-behavior sanitizers; Java ran with assertions enabled. These checks validate the small implementation and corpus, not industrial-scale performance or proof-certificate generation.
Complexity, Correctness, and Invariants
Let $n$ be the variable count, $m$ the normalized original clause count, and $L$ the total normalized original literal count. Consider growing inputs rather than treating the fixed demonstration limits as an asymptotic bound. After normalization, each clause has width at most $n$. Stable duplicate checking in the C++ and Java versions gives a conservative normalization bound $O(n+m_{\text{raw}}+nL_{\text{raw}})$; Python's sets usually make this phase cheaper. Building the watch lists is linear in the stored formula size.
DPLL. Let $N$ be recursive entries. A conservative bound for these straightforward implementations is
$$T_{\mathrm{DPLL}}=O\bigl(N(n+1)(n+m+L+1)\bigr),\qquad N\le 2^{n+1}-1.$$
The polynomial factor accounts for propagation scans and repeated whole-formula scans for pure literals. Each pure choice assigns a new variable, so at most $n$ such choices occur in one entry. The binary search tree can still be exponential; propagation and purity often shrink it without providing a polynomial-time guarantee. Auxiliary storage, including the formula and watches, is $O(n+m+L)$: the trail and recursive depth are at most $n$, and the formula is not copied at every branch.
Watched literals. Only clauses with a falsified watch are revisited, but finding a replacement can scan an entire clause. The code starts that scan at the beginning each time. Therefore one propagation call is not automatically $O(L)$, and watching does not make SAT polynomial. During a monotone propagation segment, a clause of width $k$ can incur $O(k^2)$ literal inspections; the simpler bound above allows for this work. Backtracking may cause future rescanning.
CDCL. Let $P$ be loop entries, $C$ the number of conflicts, and $Q$ the maximum live database size measured as clauses plus literal occurrences, including learned clauses. A conservative implementation-specific bound is
$$T_{\mathrm{CDCL}}=O\bigl(P(n+1)(n+Q+1)+Cn^3\bigr),\qquad S_{\mathrm{CDCL}}=O(n+Q).$$
The $n^3$ term allows for the deliberately simple list-based first-UIP analysis: at most $n$ reverse-trail resolutions per conflict, with repeated trail searches and list membership work. This is a bound for the teaching code, rather than the data structures used in optimized solvers. With no learned-clause deletion, $Q$ can grow exponentially. Restarts revisit decisions, so the binary-tree entry bound for DPLL must not be applied to CDCL's loop counter. General CNF SAT retains exponential worst-case behavior for these approaches; learning and branching heuristics exploit structure rather than removing the underlying difficulty.
Best and average cases. An empty formula is accepted after setup; an explicit empty clause is rejected during setup; a chain of units may settle the answer without any decisions. Input processing still costs time proportional to what is read and stored, and the shown early-satisfaction checks scan the database. There is no distribution-independent average-case complexity: random formulas, circuit encodings, and structured configuration instances can behave very differently.
| Approach | Time perspective | Space perspective | Best fit and limitations |
|---|---|---|---|
| Truth-table enumeration | $O(2^n(m+L+1))$ for direct clause evaluation | $O(n+m+L)$ with streamed assignments | Tiny independent oracle; systematically tries every assignment |
| DPLL with scans and pure choices | $O(N(n+1)(n+m+L+1))$ as a conservative bound here | $O(n+m+L)$ | Understand propagation and chronological undo; can repeat related failures |
| Watched CDCL with retained learning | Depends on loop count, propagation work, and analysis; bound above | $O(n+Q)$; learned database may become exponential | Reuse logical explanations across branches and restarts |
| 2-SAT implication graph and SCCs | $O(n+m)$ for clauses of width at most two | $O(n+m)$ | Polynomial algorithm for 2-CNF; does not extend directly to arbitrary-width CNF |
| Binary decision diagram | Linear in the built diagram for satisfiability queries | Diagram size can be exponential in $n$ | Repeated Boolean queries; variable ordering strongly affects construction size |
| Local search with a fixed flip budget | Bounded by the chosen budget and evaluation cost | Formula plus current assignment | A found model proves SAT; failure to find one does not prove UNSAT |
Correctness has several connected parts. Unit propagation forces a literal that every satisfying extension of the current assignment must make true. Pure-literal choice preserves the existence of at least one extension: assigning a pure variable to its satisfying polarity cannot falsify an unsatisfied clause, and already satisfied clauses remain true. DPLL then splits on the two exhaustive values of an unassigned variable and restores the parent trail before exploring its sibling.
For CDCL, resolution is sound: from $A\lor x$ and $B\lor\neg x$, derive $A\lor B$. Starting from original clauses and previously sound learned clauses, every new learned clause is entailed by the original CNF. Backjumping makes the learned clause unit without discarding any model of the original formula. Watches merely schedule the same unit/conflict checks. A SAT model satisfies every original clause; a level-zero conflict has no remaining branch decision on which it depends.
Completeness also needs progress. DPLL has finitely many binary decisions. In the shown CDCL policy, learned clauses are retained and restart budgets grow, rather than repeatedly resetting search to the same state with all learned information erased. After propagation reaches a fixed point, the engine must either finish, learn from a conflict, restart under the stated budget, or assign an unassigned variable. Correctness should not be inferred for an arbitrary restart/deletion policy substituted into the code.
Every trailed implication has a reason clause whose other literals were false when it was enqueued, and each learned clause follows by resolution from the original formula and earlier learned clauses. Propagation detects unit clauses and conflicts, trail boundaries restore branch-local assignments, and decisions cover the alternatives without losing a satisfying model.
Variants, Pitfalls, and Optimizations
Watch invariants and rollback. At a conflict-free propagation fixed point, a nonunit clause either has a true watched literal or both its watches are nonfalse; no unresolved unit is waiting to be assigned. A satisfied clause can have a false watch, so asserting “both watches are always nonfalse” is too strong. Check watch-list membership separately from assignment values. A moved watch is still a valid place to watch when assignments are undone; restore the trail suffix, not the old watch positions.
The queue head is part of assignment state. After removing a trail suffix, clamp the head to the new length so a newly enqueued opposite decision will be processed. Clear stale reason and level fields as well as values. Reasons are clause IDs, not a list of guessed assumptions. The supplied code keeps clause IDs stable and never deletes a clause that might be referenced.
Conflict analysis. First-UIP resolution proceeds backward through the trail, not by arbitrarily deleting decision literals from a conflict clause. A learned clause must be a logical consequence of the original instance. The asserting literal occupies the first watch position; a highest-level remaining literal occupies the second when there is one. This matters because the clause is attached under a partial assignment, after some falsification events have already been processed. Merely attaching it and waiting for an old event to happen again would miss its immediate unit consequence; enqueue that consequence explicitly.
Activities and restarts. Production solvers use tuned decay rates, efficient priority structures, phase selection, clause minimization, and policies for retaining or deleting learned clauses. The simple update $a_v\leftarrow a_v/2+\text{bump}_v$ emphasizes recent conflicts and stays numerically bounded here. It is a VSIDS-style variant, not a specification that every VSIDS implementation halves every score on every conflict. Restarts preserve learned clauses and scores, and preserve level-zero implications; they do not mean rebuilding an empty solver. A restart is distinct from the backjump justified by a particular learned clause.
CNF encoding. Naively distributing disjunction over conjunction can expand a formula dramatically. A Tseitin encoding introduces a variable for each subexpression and clauses enforcing its meaning, then asserts the output variable. For example, $y\leftrightarrow(a\land b)$ is represented by
$$(\neg y\lor a)\land(\neg y\lor b)\land(y\lor\neg a\lor\neg b).$$
The resulting formula is satisfiable exactly when the original asserted expression is, and auxiliary variables must be projected away when reporting original inputs. Encoding strength also affects propagation: logically correct encodings can expose different amounts of useful unit reasoning.
Related tasks. Incremental SAT reuses a clause database under changing assumptions and needs careful assumption management. AllSAT enumerates models by adding blocking clauses. Model counting requires accounting for every assignment, so the pure-literal shortcut used here cannot simply be treated as a forced truth value. MaxSAT optimizes satisfied soft constraints; SMT adds theory reasoning for objects such as integers or arrays. UNSAT proof logging and independent checking require additional machinery; this implementation returns a decision and a witness for SAT, without producing a refutation certificate.
Undo every implication in a discarded trail suffix, reset the propagation head, and preserve valid watch movement and learned information. A branch conflict is not global UNSAT, a pure literal is not necessarily a logical consequence, and a restart cannot compensate for an unsound learned clause or a missed unit propagation.
Practice Problems and Real-World Use
Begin with truth tables over two or three variables. Generate clauses containing duplicates, tautologies, units, and the empty clause; compare each solver's SAT flag and evaluate every returned model. Then test a formula whose first branch fails, a long watched clause whose watch moves more than once, and repeated solves on the same object. An UNSAT oracle check is essential because a valid model alone cannot expose false UNSAT answers.
For graph coloring, introduce $x_{v,c}$ for “vertex $v$ receives color $c$.” Give each vertex an at-least-one-color clause, pairwise at-most-one-color clauses, and a clause $\neg x_{u,c}\lor\neg x_{v,c}$ for each edge and color. More compact cardinality encodings become useful as the number of choices grows. The model is only as faithful as the encoding: omitting the at-least-one constraint would allow an uncolored vertex.
For further reading, Carnegie Mellon's DPLL lecture notes develop propagation and resolution-based learning. The original Chaff paper explains watched propagation, VSIDS, and retained information across restarts. The programs here use their own deliberately small data structures and policies; the measured traces describe these programs.
| Practice task or application | Encoding or idea tested | Difficulty |
|---|---|---|
| Empty CNF, an empty clause, and contradictory units | Distinguish truth, immediate contradiction, and ordinary branch failure | Beginner |
| The six-clause worked example | Record every trail entry, reason, queue position, and undo boundary | Intermediate |
| A four-literal clause plus root units | Move a watch, skip a false candidate, and propagate the other watch | Intermediate |
| The six-variable restart fixture | First-UIP resolution, skipping an irrelevant decision level, and retained learning | Advanced |
| Tiny graph coloring or Sudoku encodings | Encode at least one, at most one, and incompatibilities; validate decoded models | Intermediate |
| Pigeonholes into fewer holes | Generate structured UNSAT instances and compare conflicts across policies | Advanced |
| Feature or dependency configuration | Express implications, exclusions, and mandatory alternatives as clauses | Intermediate |
| Circuit equivalence or bounded counterexample search | Encode a differing output or violated property; interpret SAT witnesses within the chosen bound | Advanced |