CDCL: Learning from Conflict
How modern SAT solvers fix the three fundamental limitations of DPLL.
In Practice, you saw solvers handle "real" problems (simplified for lecture, but see the OPIUM paper for the real thing) and then hit a wall on pigeonhole. Now we look inside the solver.
This phase covers the algorithm at the heart of every modern SAT solver: CDCL (Conflict-Driven Clause Learning). We start with DPLL from last week, diagnose three specific problems with it, and then see how CDCL fixes all three by learning from its own mistakes. By the end, you will understand how the solver builds an implication graph, analyzes conflicts to extract learned clauses, and jumps back to the right point in the search.
How DPLL Works
Last week we saw the DPLL algorithm and the end-to-end SAT pipeline. Before we go further, let's make sure one piece is solid: Boolean Constraint Propagation (BCP), the deduction engine at the heart of every SAT solver.
Quick recap: clauses and assignments
A SAT solver works on a formula in CNF: a conjunction (AND) of clauses, where each clause is a disjunction (OR) of literals. For example:
A clause is satisfied if at least one of its literals is true. The formula is satisfied if every clause is satisfied.
The solver builds up a partial assignment : the set of literals it has committed to so far. For example, means is true and is false, with all other variables still unassigned.
Boolean Constraint Propagation
A unit clause is a clause where all literals except one have been assigned false. The one remaining literal is forced: it must be true, or the clause is violated. For example, if and we have the clause , then is false, leaving as the only unassigned literal. BCP forces into .
BCP applies this observation repeatedly. Find a unit clause, force its literal, simplify the formula, and check whether any new unit clauses appeared. Repeat until no unit clauses remain or a conflict is found (a clause where every literal is false under ).
BCP(F, A)
repeat
for each clause c in F
if c is conflicting under A
return conflict
if c is unit under A with literal lit
A ← A ∪ {lit}
until no new assignments
return A
BCP is pure deduction. No guessing. Every assignment it makes is forced by the formula and the current partial assignment.
The DPLL algorithm
DPLL combines BCP with search. When BCP runs out of forced assignments and no conflict has been found, the solver picks an unassigned variable, guesses a value for it, and recurses. If the guess leads to a conflict, the solver backtracks and tries the other value.
DPLL(F)
G ← BCP(F)
if G = ⊤ then return true
if G = ⊥ then return false
p ← choose(vars(G))
return DPLL(G{p ↦ ⊤}) || DPLL(G{p ↦ ⊥})
The key insight from last week: BCP collapses the search tree. On a chain of implications like with forced true and forced false, brute-force search tries all assignments. DPLL with BCP finds the contradiction in one sweep, no branching needed.
DPLL is correct. But on practical formulas with thousands of variables and structured dependencies, it falls apart. Three specific reasons.
Three Problems with DPLL
No learning. When DPLL backtracks, it throws away everything it learned about why the current partial assignment failed. The same conflict can be rediscovered exponentially many times, because the solver has no memory of what went wrong.
Chronological backtracking. DPLL always backtracks one level. If a conflict was caused by a decision made 10 levels ago, the solver wastes time undoing and redoing irrelevant decisions before it gets back to the actual source of the problem.
Naive decisions. DPLL picks the next variable to branch on arbitrarily. It ignores everything it has seen during the search so far. Some variables matter much more than others for resolving the current sub-problem.
These are not minor inefficiencies. They are fundamental design limitations. Every competitive SAT solver today uses a different algorithm called CDCL (Conflict-Driven Clause Learning) that fixes all three.
The CDCL Algorithm
CDCL extends DPLL with three fixes, one for each problem:
- Learning: when BCP hits a conflict, the solver analyzes it and adds a conflict clause that summarizes the root cause. The solver never makes the same mistake twice.
- Non-chronological backtracking: instead of backing up one level, the solver jumps back to the decision level where the conflict actually originated.
- Decision heuristics: instead of picking variables arbitrarily, the solver chooses variables that are most involved in recent conflicts.
Here is the algorithm:
CDCL(F)
A ← {}
if BCP(F, A) = conflict then return false
level ← 0
while hasUnassignedVars(F)
level ← level + 1
A ← A ∪ { Decide(F, A) }
while BCP(F, A) = conflict
⟨b, c⟩ ← AnalyzeConflict()
F ← F ∪ {c}
if b < 0 then return false
else
Backtrack(F, A, b)
level ← b
return true
The outer loop makes decisions. After each decision, BCP propagates forced assignments. If BCP finds no conflict, the solver makes another decision. If BCP hits a conflict, the inner loop takes over: AnalyzeConflict computes a conflict clause c and a backtrack level b. The clause is added to the formula (learning), and the solver jumps back to level b (non-chronological backtracking). Decide uses heuristics to choose which variable to branch on next.
If AnalyzeConflict returns b < 0, the conflict occurred at decision level 0, which means the formula is unsatisfiable: no decisions were involved, so the conflict is unconditional.
Three vocabulary terms to keep track of:
- Decision level: the depth of the current decision in the search. Level 0 is before any decisions. Each call to
Decideincrements the level. - Conflict clause: a clause learned from a conflict that blocks the partial assignment that caused it. Added to the formula permanently.
- Asserting clause: a conflict clause that becomes unit immediately after backtracking. It forces a new assignment by BCP without any new decisions.
CDCL by Example
Before defining the machinery formally, let's watch CDCL solve a problem.
Consider the formula with six clauses over eight variables:
The solver's job: find values for that satisfy all six clauses, or prove no such values exist.
Decision 1: = true at level 1
The solver picks and sets it to true. Now check: does any clause become unit? Look at each clause that contains :
- . The literal is now false, but and are still unassigned. Two unassigned literals remain. Not unit.
- . Same situation: is false, but and are still unassigned. Not unit.
BCP finds nothing to propagate. The solver makes another decision.
In the diagrams below, each node shows a literal and the decision level at which it was assigned. A positive literal like x1 @1 means was set to true at level 1. A negated literal like ¬x7 @3 means was set to false at level 3.
graph LR
accTitle: Implication graph after decision 1
accDescr: Only x1 at level 1 is visible. All other nodes and edges are transparent placeholders to preserve layout.
x1["x1 @1"]
x8["x8 @2"]
nx7["¬x7 @3"]
nx5["¬x5 @3"]
nx6["¬x6 @3"]
x4["x4 @3"]
nx3["¬x3 @3"]
x2["x2 @3"]
K["κ"]
nx7 -- "c5" --> nx5
nx7 -- "c6" --> nx6
x8 -- "c6" --> nx6
nx5 -- "c4" --> x4
nx6 -- "c4" --> x4
x4 -- "c3" --> nx3
x4 -- "c1" --> x2
x1 -- "c1" --> x2
x1 -- "c2" --> K
x2 -- "c2" --> K
nx3 -- "c2" --> K
style x1 fill:#d4edda,stroke:#28a745
style x8 fill:transparent,stroke:transparent,color:transparent
style nx7 fill:transparent,stroke:transparent,color:transparent
style nx5 fill:transparent,stroke:transparent,color:transparent
style nx6 fill:transparent,stroke:transparent,color:transparent
style x4 fill:transparent,stroke:transparent,color:transparent
style nx3 fill:transparent,stroke:transparent,color:transparent
style x2 fill:transparent,stroke:transparent,color:transparent
style K fill:transparent,stroke:transparent,color:transparent
linkStyle 0 stroke:transparent,color:transparent
linkStyle 1 stroke:transparent,color:transparent
linkStyle 2 stroke:transparent,color:transparent
linkStyle 3 stroke:transparent,color:transparent
linkStyle 4 stroke:transparent,color:transparent
linkStyle 5 stroke:transparent,color:transparent
linkStyle 6 stroke:transparent,color:transparent
linkStyle 7 stroke:transparent,color:transparent
linkStyle 8 stroke:transparent,color:transparent
linkStyle 9 stroke:transparent,color:transparent
linkStyle 10 stroke:transparent,color:transparentDecision 2: = true at level 2
The solver picks and sets it to true. Check the clauses:
- . The literal is now false, but and are still unassigned. Not unit yet.
Nothing else changes. BCP finds nothing. One more decision.
graph LR
accTitle: Implication graph after decision 2
accDescr: Two decision nodes visible, x1 at level 1 and x8 at level 2. All other nodes and edges are hidden.
x1["x1 @1"]
x8["x8 @2"]
nx7["¬x7 @3"]
nx5["¬x5 @3"]
nx6["¬x6 @3"]
x4["x4 @3"]
nx3["¬x3 @3"]
x2["x2 @3"]
K["κ"]
nx7 -- "c5" --> nx5
nx7 -- "c6" --> nx6
x8 -- "c6" --> nx6
nx5 -- "c4" --> x4
nx6 -- "c4" --> x4
x4 -- "c3" --> nx3
x4 -- "c1" --> x2
x1 -- "c1" --> x2
x1 -- "c2" --> K
x2 -- "c2" --> K
nx3 -- "c2" --> K
style x1 fill:#d4edda,stroke:#28a745
style x8 fill:#d4edda,stroke:#28a745
style nx7 fill:transparent,stroke:transparent,color:transparent
style nx5 fill:transparent,stroke:transparent,color:transparent
style nx6 fill:transparent,stroke:transparent,color:transparent
style x4 fill:transparent,stroke:transparent,color:transparent
style nx3 fill:transparent,stroke:transparent,color:transparent
style x2 fill:transparent,stroke:transparent,color:transparent
style K fill:transparent,stroke:transparent,color:transparent
linkStyle 0 stroke:transparent,color:transparent
linkStyle 1 stroke:transparent,color:transparent
linkStyle 2 stroke:transparent,color:transparent
linkStyle 3 stroke:transparent,color:transparent
linkStyle 4 stroke:transparent,color:transparent
linkStyle 5 stroke:transparent,color:transparent
linkStyle 6 stroke:transparent,color:transparent
linkStyle 7 stroke:transparent,color:transparent
linkStyle 8 stroke:transparent,color:transparent
linkStyle 9 stroke:transparent,color:transparent
linkStyle 10 stroke:transparent,color:transparentDecision 3: = false at level 3
The solver picks and sets it to false. Now things start moving.
graph LR
accTitle: Implication graph after decision 3
accDescr: Three decision nodes visible, x1 at level 1, x8 at level 2, and x7 false at level 3. All other nodes and edges are hidden. BCP is about to fire.
x1["x1 @1"]
x8["x8 @2"]
nx7["¬x7 @3"]
nx5["¬x5 @3"]
nx6["¬x6 @3"]
x4["x4 @3"]
nx3["¬x3 @3"]
x2["x2 @3"]
K["κ"]
nx7 -- "c5" --> nx5
nx7 -- "c6" --> nx6
x8 -- "c6" --> nx6
nx5 -- "c4" --> x4
nx6 -- "c4" --> x4
x4 -- "c3" --> nx3
x4 -- "c1" --> x2
x1 -- "c1" --> x2
x1 -- "c2" --> K
x2 -- "c2" --> K
nx3 -- "c2" --> K
style x1 fill:#d4edda,stroke:#28a745
style x8 fill:#d4edda,stroke:#28a745
style nx7 fill:#d4edda,stroke:#28a745
style nx5 fill:transparent,stroke:transparent,color:transparent
style nx6 fill:transparent,stroke:transparent,color:transparent
style x4 fill:transparent,stroke:transparent,color:transparent
style nx3 fill:transparent,stroke:transparent,color:transparent
style x2 fill:transparent,stroke:transparent,color:transparent
style K fill:transparent,stroke:transparent,color:transparent
linkStyle 0 stroke:transparent,color:transparent
linkStyle 1 stroke:transparent,color:transparent
linkStyle 2 stroke:transparent,color:transparent
linkStyle 3 stroke:transparent,color:transparent
linkStyle 4 stroke:transparent,color:transparent
linkStyle 5 stroke:transparent,color:transparent
linkStyle 6 stroke:transparent,color:transparent
linkStyle 7 stroke:transparent,color:transparent
linkStyle 8 stroke:transparent,color:transparent
linkStyle 9 stroke:transparent,color:transparent
linkStyle 10 stroke:transparent,color:transparentBCP propagation chain
Step 1. Look at . The literal is false. The only unassigned literal left is . This clause is unit. BCP has no choice: must be false.
BCP at level 3, forced by .
graph LR
accTitle: Implication graph after BCP step 1
accDescr: Three decision nodes plus x5 false at level 3, forced by c5. Edge from x7 to x5 labeled c5 is now visible.
x1["x1 @1"]
x8["x8 @2"]
nx7["¬x7 @3"]
nx5["¬x5 @3"]
nx6["¬x6 @3"]
x4["x4 @3"]
nx3["¬x3 @3"]
x2["x2 @3"]
K["κ"]
nx7 -- "c5" --> nx5
nx7 -- "c6" --> nx6
x8 -- "c6" --> nx6
nx5 -- "c4" --> x4
nx6 -- "c4" --> x4
x4 -- "c3" --> nx3
x4 -- "c1" --> x2
x1 -- "c1" --> x2
x1 -- "c2" --> K
x2 -- "c2" --> K
nx3 -- "c2" --> K
style x1 fill:#d4edda,stroke:#28a745
style x8 fill:#d4edda,stroke:#28a745
style nx7 fill:#d4edda,stroke:#28a745
style nx5 fill:#fff,stroke:#333
style nx6 fill:transparent,stroke:transparent,color:transparent
style x4 fill:transparent,stroke:transparent,color:transparent
style nx3 fill:transparent,stroke:transparent,color:transparent
style x2 fill:transparent,stroke:transparent,color:transparent
style K fill:transparent,stroke:transparent,color:transparent
linkStyle 1 stroke:transparent,color:transparent
linkStyle 2 stroke:transparent,color:transparent
linkStyle 3 stroke:transparent,color:transparent
linkStyle 4 stroke:transparent,color:transparent
linkStyle 5 stroke:transparent,color:transparent
linkStyle 6 stroke:transparent,color:transparent
linkStyle 7 stroke:transparent,color:transparent
linkStyle 8 stroke:transparent,color:transparent
linkStyle 9 stroke:transparent,color:transparent
linkStyle 10 stroke:transparent,color:transparentStep 2. Look at . We already know is false and is true (so is false). The only unassigned literal left is . Unit. BCP forces to false.
BCP at level 3, forced by .
graph LR
accTitle: Implication graph after BCP step 2
accDescr: Three decision nodes plus x5 false and x6 false at level 3. Edges from x7 to x5 via c5, and from both x7 and x8 to x6 via c6 are visible.
x1["x1 @1"]
x8["x8 @2"]
nx7["¬x7 @3"]
nx5["¬x5 @3"]
nx6["¬x6 @3"]
x4["x4 @3"]
nx3["¬x3 @3"]
x2["x2 @3"]
K["κ"]
nx7 -- "c5" --> nx5
nx7 -- "c6" --> nx6
x8 -- "c6" --> nx6
nx5 -- "c4" --> x4
nx6 -- "c4" --> x4
x4 -- "c3" --> nx3
x4 -- "c1" --> x2
x1 -- "c1" --> x2
x1 -- "c2" --> K
x2 -- "c2" --> K
nx3 -- "c2" --> K
style x1 fill:#d4edda,stroke:#28a745
style x8 fill:#d4edda,stroke:#28a745
style nx7 fill:#d4edda,stroke:#28a745
style nx5 fill:#fff,stroke:#333
style nx6 fill:#fff,stroke:#333
style x4 fill:transparent,stroke:transparent,color:transparent
style nx3 fill:transparent,stroke:transparent,color:transparent
style x2 fill:transparent,stroke:transparent,color:transparent
style K fill:transparent,stroke:transparent,color:transparent
linkStyle 3 stroke:transparent,color:transparent
linkStyle 4 stroke:transparent,color:transparent
linkStyle 5 stroke:transparent,color:transparent
linkStyle 6 stroke:transparent,color:transparent
linkStyle 7 stroke:transparent,color:transparent
linkStyle 8 stroke:transparent,color:transparent
linkStyle 9 stroke:transparent,color:transparent
linkStyle 10 stroke:transparent,color:transparentStep 3. Look at . Both and are false. The only unassigned literal is . Unit. BCP forces to true.
BCP at level 3, forced by .
graph LR
accTitle: Implication graph after BCP step 3
accDescr: Three decision nodes plus x5 false, x6 false, and x4 true at level 3. Edges from x5 and x6 to x4 via c4 are now visible.
x1["x1 @1"]
x8["x8 @2"]
nx7["¬x7 @3"]
nx5["¬x5 @3"]
nx6["¬x6 @3"]
x4["x4 @3"]
nx3["¬x3 @3"]
x2["x2 @3"]
K["κ"]
nx7 -- "c5" --> nx5
nx7 -- "c6" --> nx6
x8 -- "c6" --> nx6
nx5 -- "c4" --> x4
nx6 -- "c4" --> x4
x4 -- "c3" --> nx3
x4 -- "c1" --> x2
x1 -- "c1" --> x2
x1 -- "c2" --> K
x2 -- "c2" --> K
nx3 -- "c2" --> K
style x1 fill:#d4edda,stroke:#28a745
style x8 fill:#d4edda,stroke:#28a745
style nx7 fill:#d4edda,stroke:#28a745
style nx5 fill:#fff,stroke:#333
style nx6 fill:#fff,stroke:#333
style x4 fill:#fff,stroke:#333
style nx3 fill:transparent,stroke:transparent,color:transparent
style x2 fill:transparent,stroke:transparent,color:transparent
style K fill:transparent,stroke:transparent,color:transparent
linkStyle 5 stroke:transparent,color:transparent
linkStyle 6 stroke:transparent,color:transparent
linkStyle 7 stroke:transparent,color:transparent
linkStyle 8 stroke:transparent,color:transparent
linkStyle 9 stroke:transparent,color:transparent
linkStyle 10 stroke:transparent,color:transparentStep 4. Look at . We just set to true, so is false. The only unassigned literal is . Unit. BCP forces to false.
BCP at level 3, forced by .
graph LR
accTitle: Implication graph after BCP step 4
accDescr: Three decision nodes plus x5 false, x6 false, x4 true, and x3 false at level 3. Edge from x4 to x3 via c3 is now visible.
x1["x1 @1"]
x8["x8 @2"]
nx7["¬x7 @3"]
nx5["¬x5 @3"]
nx6["¬x6 @3"]
x4["x4 @3"]
nx3["¬x3 @3"]
x2["x2 @3"]
K["κ"]
nx7 -- "c5" --> nx5
nx7 -- "c6" --> nx6
x8 -- "c6" --> nx6
nx5 -- "c4" --> x4
nx6 -- "c4" --> x4
x4 -- "c3" --> nx3
x4 -- "c1" --> x2
x1 -- "c1" --> x2
x1 -- "c2" --> K
x2 -- "c2" --> K
nx3 -- "c2" --> K
style x1 fill:#d4edda,stroke:#28a745
style x8 fill:#d4edda,stroke:#28a745
style nx7 fill:#d4edda,stroke:#28a745
style nx5 fill:#fff,stroke:#333
style nx6 fill:#fff,stroke:#333
style x4 fill:#fff,stroke:#333
style nx3 fill:#fff,stroke:#333
style x2 fill:transparent,stroke:transparent,color:transparent
style K fill:transparent,stroke:transparent,color:transparent
linkStyle 6 stroke:transparent,color:transparent
linkStyle 7 stroke:transparent,color:transparent
linkStyle 8 stroke:transparent,color:transparent
linkStyle 9 stroke:transparent,color:transparent
linkStyle 10 stroke:transparent,color:transparentStep 5. Look at . We know is true (so is false) and is true (so is false). The only unassigned literal is . Unit. BCP forces to true.
BCP at level 3, forced by .
graph LR
accTitle: Implication graph after BCP step 5
accDescr: Three decision nodes plus all five implied nodes at level 3. Edges from x4 and x1 to x2 via c1 are now visible. Only kappa remains hidden.
x1["x1 @1"]
x8["x8 @2"]
nx7["¬x7 @3"]
nx5["¬x5 @3"]
nx6["¬x6 @3"]
x4["x4 @3"]
nx3["¬x3 @3"]
x2["x2 @3"]
K["κ"]
nx7 -- "c5" --> nx5
nx7 -- "c6" --> nx6
x8 -- "c6" --> nx6
nx5 -- "c4" --> x4
nx6 -- "c4" --> x4
x4 -- "c3" --> nx3
x4 -- "c1" --> x2
x1 -- "c1" --> x2
x1 -- "c2" --> K
x2 -- "c2" --> K
nx3 -- "c2" --> K
style x1 fill:#d4edda,stroke:#28a745
style x8 fill:#d4edda,stroke:#28a745
style nx7 fill:#d4edda,stroke:#28a745
style nx5 fill:#fff,stroke:#333
style nx6 fill:#fff,stroke:#333
style x4 fill:#fff,stroke:#333
style nx3 fill:#fff,stroke:#333
style x2 fill:#fff,stroke:#333
style K fill:transparent,stroke:transparent,color:transparent
linkStyle 8 stroke:transparent,color:transparent
linkStyle 9 stroke:transparent,color:transparent
linkStyle 10 stroke:transparent,color:transparentStep 6. Look at . Check each literal: is true so is false. is true so is false. is false so is false. Every literal in this clause is false.
Conflict. There is no way to satisfy .
The implication graph
Each propagation step above was forced by a clause acting on previous assignments. We can draw this chain of reasoning as a directed graph. Decision nodes (our guesses) are the roots. Implied nodes (forced by BCP) follow from them. Edges are labeled with the clause that caused the implication.
graph LR
accTitle: Implication graph for CDCL example
accDescr: Directed graph showing three decision nodes (x1 at level 1, x8 at level 2, neg-x7 at level 3) and five implied nodes leading to a conflict node. Edges are labeled with the clause that forced each implication.
x1["x1 @1"]
x8["x8 @2"]
nx7["¬x7 @3"]
nx5["¬x5 @3"]
nx6["¬x6 @3"]
x4["x4 @3"]
nx3["¬x3 @3"]
x2["x2 @3"]
K["κ"]
nx7 -- "c5" --> nx5
nx7 -- "c6" --> nx6
x8 -- "c6" --> nx6
nx5 -- "c4" --> x4
nx6 -- "c4" --> x4
x4 -- "c3" --> nx3
x4 -- "c1" --> x2
x1 -- "c1" --> x2
x1 -- "c2" --> K
x2 -- "c2" --> K
nx3 -- "c2" --> K
style x1 fill:#d4edda,stroke:#28a745
style x8 fill:#d4edda,stroke:#28a745
style nx7 fill:#d4edda,stroke:#28a745
style K fill:#f8d7da,stroke:#dc3545The green nodes are decisions (our guesses). The red node is the conflict. Everything in between was forced by BCP. The graph is a complete record of how the solver's reasoning led to a contradiction.
What DPLL would do
DPLL would backtrack one level: undo the decision at level 3 and try instead. If that also leads to conflict, go back to level 2 and try . One level at a time.
What CDCL does instead
CDCL looks at the implication graph and asks: what actually caused this conflict?
The conflict happened at , which involves , , and . Look at where those came from. was forced by because of and . was forced false by because of . The real problem is the combination of (decided at level 1) and (implied at level 3). If both are true, you get a conflict no matter what.
The conflict clause is : "do not set both and to true."
The solver adds this clause to the formula, then backtracks to level 1 (where was decided).
graph LR
accTitle: Implication graph after backtracking to level 1
accDescr: Only x1 at level 1 remains. All level 2 and level 3 nodes are gone. The learned clause neg-x1 or neg-x4 is now unit, forcing x4 to false.
x1["x1 @1"]
x8["x8 @2"]
nx7["¬x7 @3"]
nx5["¬x5 @3"]
nx6["¬x6 @3"]
x4["x4 @3"]
nx3["¬x3 @3"]
x2["x2 @3"]
K["κ"]
nx7 -- "c5" --> nx5
nx7 -- "c6" --> nx6
x8 -- "c6" --> nx6
nx5 -- "c4" --> x4
nx6 -- "c4" --> x4
x4 -- "c3" --> nx3
x4 -- "c1" --> x2
x1 -- "c1" --> x2
x1 -- "c2" --> K
x2 -- "c2" --> K
nx3 -- "c2" --> K
style x1 fill:#d4edda,stroke:#28a745
style x8 fill:transparent,stroke:transparent,color:transparent
style nx7 fill:transparent,stroke:transparent,color:transparent
style nx5 fill:transparent,stroke:transparent,color:transparent
style nx6 fill:transparent,stroke:transparent,color:transparent
style x4 fill:transparent,stroke:transparent,color:transparent
style nx3 fill:transparent,stroke:transparent,color:transparent
style x2 fill:transparent,stroke:transparent,color:transparent
style K fill:transparent,stroke:transparent,color:transparent
linkStyle 0 stroke:transparent,color:transparent
linkStyle 1 stroke:transparent,color:transparent
linkStyle 2 stroke:transparent,color:transparent
linkStyle 3 stroke:transparent,color:transparent
linkStyle 4 stroke:transparent,color:transparent
linkStyle 5 stroke:transparent,color:transparent
linkStyle 6 stroke:transparent,color:transparent
linkStyle 7 stroke:transparent,color:transparent
linkStyle 8 stroke:transparent,color:transparent
linkStyle 9 stroke:transparent,color:transparent
linkStyle 10 stroke:transparent,color:transparentAt level 1, is still true, so the learned clause immediately becomes unit and forces . The solver continues from there with new information it did not have before.
Notice what happened. The solver skipped level 2 entirely. The decision about at level 2 was irrelevant to the conflict. DPLL would have wasted time exploring both values of before eventually reaching the same conclusion.
From a single conflict, the solver learned something that applies globally: and cannot both be true. This is not just avoiding the specific sequence of decisions that failed. Every future branch of the search where and would both be true is now ruled out immediately by BCP. One conflict, analyzed carefully, can prune an exponential number of assignments that would have failed for the same reason.
Implication Graphs
The diagram above is an implication graph: a directed acyclic graph that records every decision and deduction the solver made. Let's define the pieces.
An implication graph has:
- Vertices: each vertex is a literal in the current partial assignment, tagged with the decision level at which it was assigned. The special node represents a conflict.
- Edges: if BCP forced literal because clause became unit, there is an edge from each other literal in to , labeled with . These edges record why was forced.
Two kinds of vertices:
- A decision literal was chosen by
Decide. It has no incoming edges. In our diagram, these are the green nodes: @1, @2, @3. - An implied literal was forced by BCP. Its incoming edges point back to the literals and the clause that caused the forcing. In our diagram, @3, @3, @3, @3, and @3 are all implied.
The clause that forced an implied literal is called its antecedent. For example, look at how entered the assignment. The clause had assigned false, leaving as the only unassigned literal. BCP forced . So is the antecedent of , and the implication graph has an edge from to labeled .
Similarly, has two incoming edges (from and , both labeled ) because needed both and before it became unit.
An implication graph that includes the conflict node is called a conflict graph. It is a complete record of why the current partial assignment leads to a contradiction. If you have the conflict graph, you can work backward to recover the clauses that produced it. That is what the exercises below ask you to do.
Conflict Analysis
When CDCL hits a conflict, it needs to answer two questions: what went wrong, and how far should we backtrack? The implication graph has the answers.
Separating cuts
A separating cut in a conflict graph is a set of edges that breaks all paths from the decision nodes to . Every separating cut divides the graph into a reason side (containing the decisions) and a conflict side (containing ).
The nodes on the reason side that have at least one edge crossing to the conflict side form a sufficient condition for the conflict. To build the conflict clause: take each such node, negate its literal, and OR them together.
In our running example, consider two different cuts:
Cut 1 (conservative): separate all three decision nodes from everything else. The nodes on the reason side with edges crossing the cut are @1 (true), @2 (true), and @3 (false, i.e., is false). Negate each: , , . The conflict clause is:
This says: "at least one of these must hold: is false, is false, or is true." It is a valid conflict clause, but it involves all three decisions, including , which had nothing to do with the conflict.
graph LR
accTitle: Conflict graph with conservative cut
accDescr: The full implication graph with the three decision nodes highlighted in blue on the reason side. All other nodes are on the conflict side. The conservative cut produces the conflict clause neg-x1 or neg-x8 or x7.
subgraph Reason
x1["x1 @1"]
x8["x8 @2"]
nx7["¬x7 @3"]
end
subgraph Conflict
nx5["¬x5 @3"]
nx6["¬x6 @3"]
x4["x4 @3"]
nx3["¬x3 @3"]
x2["x2 @3"]
K["κ"]
end
nx7 -- "c5" --> nx5
nx7 -- "c6" --> nx6
x8 -- "c6" --> nx6
nx5 -- "c4" --> x4
nx6 -- "c4" --> x4
x4 -- "c3" --> nx3
x4 -- "c1" --> x2
x1 -- "c1" --> x2
x1 -- "c2" --> K
x2 -- "c2" --> K
nx3 -- "c2" --> K
style x1 fill:#cce5ff,stroke:#004085
style x8 fill:#cce5ff,stroke:#004085
style nx7 fill:#cce5ff,stroke:#004085
style K fill:#f8d7da,stroke:#dc3545Cut 2 (tighter): cut between @3 and the nodes to its right (, , ). The nodes on the reason side with edges crossing are @1 (true) and @3 (true). Negate each: , . The conflict clause is:
graph LR
accTitle: Conflict graph with tighter cut at first UIP
accDescr: The full implication graph with x1 and x4 highlighted in blue on the reason side. The cut passes between x4 and the nodes x2, x3, kappa on the conflict side. This tighter cut produces the shorter conflict clause neg-x1 or neg-x4.
subgraph Reason
x1["x1 @1"]
x8["x8 @2"]
nx7["¬x7 @3"]
nx5["¬x5 @3"]
nx6["¬x6 @3"]
x4["x4 @3"]
end
subgraph Conflict
nx3["¬x3 @3"]
x2["x2 @3"]
K["κ"]
end
nx7 -- "c5" --> nx5
nx7 -- "c6" --> nx6
x8 -- "c6" --> nx6
nx5 -- "c4" --> x4
nx6 -- "c4" --> x4
x4 -- "c3" --> nx3
x4 -- "c1" --> x2
x1 -- "c1" --> x2
x1 -- "c2" --> K
x2 -- "c2" --> K
nx3 -- "c2" --> K
style x1 fill:#cce5ff,stroke:#004085
style x4 fill:#cce5ff,stroke:#004085
style K fill:#f8d7da,stroke:#dc3545Just two literals instead of three. This clause says: "do not set both and to true." It applies no matter what or are.
A shorter conflict clause is better. It rules out a larger region of the search space. The two-literal clause blocks every assignment where both are true, regardless of what the other six variables do. The three-literal clause is more restrictive: it only blocks assignments where is true AND is true AND is false.
Unique implication points
How do we find the best cut? The answer involves unique implication points (UIPs).
A UIP is any node in the implication graph (other than ) that lies on every path from the current decision literal to . The current decision literal is always a UIP (trivially, every path from it to passes through it).
The first UIP is the UIP closest to . Cutting after the first UIP gives the shortest conflict clause.
In our example, the decision at level 3 is . The first UIP is @3: every path from to passes through . Cutting after gives the conflict clause .
Binary resolution
AnalyzeConflict finds the first UIP clause using binary resolution: a rule for combining two clauses that share a variable in opposite polarities.
Here is a concrete example first. Suppose we have two clauses:
The variable appears positive in the first clause and negative in the second. We can resolve on : remove from the first clause and from the second, and combine what remains:
The general rule:
The variable is the resolution variable. The result (the resolvent) contains everything from both clauses except and . If a literal appears in both clauses (like in our example), it appears once in the resolvent.
The AnalyzeConflict algorithm
AnalyzeConflict applies binary resolution backward from the conflict until the resulting clause has exactly one literal at the current decision level. That literal is the negation of the first UIP.
AnalyzeConflict()
d ← level(conflict)
if d = 0 then return -1
c ← antecedent(conflict)
while !oneLitAtLevel(c, d)
t ← lastAssignedLitAtLevel(c, d)
v ← varOfLit(t)
a ← antecedent(t)
c ← resolve(a, c, v)
b ← assertingLevel(c)
return ⟨b, c⟩
The asserting level is the second-highest decision level among the literals in . After backtracking to this level, the clause has exactly one unassigned literal (the first UIP), so it becomes unit and BCP immediately propagates it. If is unary (only one literal), the asserting level is 0.
Walked example
Trace AnalyzeConflict on our running example. The conflict is at level 3.
Start:
Three literals: @1, @3, @3. Two literals at level 3. Not done yet.
Step 1: The last assigned literal at level 3 in is . Its antecedent is . Resolve with on :
Now . Literals: @1, @3, @3. Still two at level 3.
Step 2: The last assigned literal at level 3 in is . Its antecedent is . Resolve with on :
Now . Literals: @1, @3. One literal at level 3. Done.
Result: conflict clause , backtrack level = 1 (the level of , the second-highest level in ).
Exercises
Exercise 1
Here is a conflict graph. What clauses gave rise to it?
graph LR
accTitle: Exercise 1 conflict graph
accDescr: A conflict graph with decision node x1 at level 6, implied nodes x2 x3 x4 at level 6, a node neg-x5 at level 3, and a conflict node kappa at level 6. Students should determine what clauses produced this graph.
x1["x1 @6"]
nx5["¬x5 @3"]
x2["x2 @6"]
x3["x3 @6"]
x4["x4 @6"]
K["κ @6"]
x1 -- "c1" --> x2
x1 -- "c2" --> x3
nx5 -- "c2" --> x3
x2 -- "c3" --> x4
x4 -- "c4" --> K
x3 -- "c4" --> K
style x1 fill:#d4edda,stroke:#28a745
style nx5 fill:#d4edda,stroke:#28a745
style K fill:#f8d7da,stroke:#dc3545Work backward from the edges. If there is an edge from to labeled , then must contain (which was falsified, making the clause unit) and (the literal that was forced). For edges with two sources, both source literals appear negated in the clause.
Answer
- \(c_1 = \neg x_1 \lor x_2\) - \(c_2 = \neg x_1 \lor x_3 \lor x_5\) - \(c_3 = \neg x_2 \lor x_4\) - \(c_4 = \neg x_3 \lor \neg x_4\)Exercise 2
Same graph, but is now at level 0 instead of level 3. What clauses gave rise to it?
graph LR
accTitle: Exercise 2 conflict graph
accDescr: Same graph as exercise 1 but neg-x5 is at level 0 instead of level 3, indicating it was forced by a unary clause before any decisions were made.
x1["x1 @6"]
nx5["¬x5 @0"]
x2["x2 @6"]
x3["x3 @6"]
x4["x4 @6"]
K["κ @6"]
x1 -- "c1" --> x2
x1 -- "c2" --> x3
nx5 -- "c2" --> x3
x2 -- "c3" --> x4
x4 -- "c4" --> K
x3 -- "c4" --> K
style x1 fill:#d4edda,stroke:#28a745
style nx5 fill:#d4edda,stroke:#28a745
style K fill:#f8d7da,stroke:#dc3545Answer
The same four clauses as Exercise 1, plus one more: - \(c_k = \neg x_5\) Assignments at level 0 are forced by unary clauses before any decisions are made. The unary clause \(\neg x_5\) is what put \(\neg x_5\) into the partial assignment at level 0.Decision Heuristics: VSIDS
DPLL picks the next variable to branch on arbitrarily. CDCL can do better.
One simple idea is DLIS (Dynamic Largest Individual Sum): pick the literal that satisfies the most currently unresolved clauses. This is intuitive, but expensive. Computing it requires scanning all clauses on every decision.
The standard approach in modern solvers is VSIDS (Variable State Independent Decaying Sum), introduced in the zChaff solver:
- Each literal gets a score, initialized to the number of clauses it appears in.
- Whenever a conflict clause is learned, bump the scores of all literals in that clause.
- Periodically divide all scores by a constant (e.g., 2).
- Always pick the literal with the highest score.
For example, suppose the solver has five variables and learns the conflict clause . Before learning it, the scores might look like:
| Literal | Score before | Score after |
|---|---|---|
| 3 | 3 | |
| 2 | 3 | |
| 4 | 4 | |
| 1 | 2 | |
| 3 | 3 |
The literals and appeared in the conflict clause, so their scores get bumped. After a periodic decay (divide everything by 2), recent conflict participants stay near the top while old scores shrink.
The periodic decay is the key idea. It keeps the solver focused on variables involved in recent conflicts. Variables that mattered 1000 conflicts ago but have not appeared in a conflict since will have their scores decayed to near zero. Variables that keep showing up in conflict clauses stay at the top.
Decision time is constant when literals are kept in a sorted list.
Watched Literals
Solvers spend most of their time in BCP, so BCP must be fast. A naive implementation scans all unsatisfied clauses after every assignment. This is too slow for formulas with millions of clauses.
The watched literals scheme, also from zChaff, is based on a simple observation: a clause cannot become unit or conflicting until it has fewer than two unassigned literals. So instead of scanning every clause, we only need to pay attention when a clause gets close to becoming unit.
The idea: for each unresolved clause, pick two unassigned literals to watch. Only examine a clause when one of its watched literals gets assigned false. When that happens, find another unassigned literal in the clause to watch instead. If there is only one unassigned literal left, the clause is unit and BCP fires.
For example, consider the clause . We pick two unassigned literals to watch, say and . As long as both are unassigned, this clause cannot be unit (it has at least two unassigned literals), so we ignore it. If gets assigned false, we look at the clause and find another unassigned literal to replace it, say . Now we watch and . Only when we cannot find a replacement (because all other literals are false) do we know the clause is unit.
The best part: when the solver backtracks and unassigns a literal, nothing needs to happen to the watched literals. The watched literals might now point at assigned literals, but that is fine: they will get updated the next time BCP examines the clause. This means backtracking is cheap, which matters because CDCL backtracks frequently.
What Comes Next
After the break, we move to Studio for the first reading discussion. The readings this week all question whether formal methods are worth the investment. You have just spent two hours seeing what solvers can and cannot do. Bring that experience to the discussion.