Skip to main content
  (Week 2)

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 F in CNF: a conjunction (AND) of clauses, where each clause is a disjunction (OR) of literals. For example:

F=(¬x1x2)(x1¬x3x4)(x2x3)

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 A: the set of literals it has committed to so far. For example, A={x1,¬x3} means x1 is true and x3 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 A={x1} and we have the clause (¬x1x2), then ¬x1 is false, leaving x2 as the only unassigned literal. BCP forces x2 into A.

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 A).

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 x1x2x12 with x1 forced true and x12 forced false, brute-force search tries all 212 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:

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:

CDCL by Example

Before defining the machinery formally, let's watch CDCL solve a problem.

Consider the formula F with six clauses over eight variables:

c1=¬x1x2¬x4c2=¬x1¬x2x3c3=¬x3¬x4c4=x4x5x6c5=¬x5x7c6=¬x6x7¬x8

The solver's job: find values for x1,,x8 that satisfy all six clauses, or prove no such values exist.

Decision 1: x1 = true at level 1

The solver picks x1 and sets it to true. Now check: does any clause become unit? Look at each clause that contains x1:

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 x1 was set to true at level 1. A negated literal like ¬x7 @3 means x7 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:transparent

Decision 2: x8 = true at level 2

The solver picks x8 and sets it to true. Check the clauses:

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:transparent

Decision 3: x7 = false at level 3

The solver picks x7 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:transparent

BCP propagation chain

Step 1. Look at c5=¬x5x7. The literal x7 is false. The only unassigned literal left is ¬x5. This clause is unit. BCP has no choice: x5 must be false.

BCP ¬x5 at level 3, forced by c5.

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:transparent

Step 2. Look at c6=¬x6x7¬x8. We already know x7 is false and x8 is true (so ¬x8 is false). The only unassigned literal left is ¬x6. Unit. BCP forces x6 to false.

BCP ¬x6 at level 3, forced by c6.

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:transparent

Step 3. Look at c4=x4x5x6. Both x5 and x6 are false. The only unassigned literal is x4. Unit. BCP forces x4 to true.

BCP x4 at level 3, forced by c4.

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:transparent

Step 4. Look at c3=¬x3¬x4. We just set x4 to true, so ¬x4 is false. The only unassigned literal is ¬x3. Unit. BCP forces x3 to false.

BCP ¬x3 at level 3, forced by c3.

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:transparent

Step 5. Look at c1=¬x1x2¬x4. We know x1 is true (so ¬x1 is false) and x4 is true (so ¬x4 is false). The only unassigned literal is x2. Unit. BCP forces x2 to true.

BCP x2 at level 3, forced by c1.

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:transparent

Step 6. Look at c2=¬x1¬x2x3. Check each literal: x1 is true so ¬x1 is false. x2 is true so ¬x2 is false. x3 is false so x3 is false. Every literal in this clause is false.

Conflict. There is no way to satisfy c2.

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:#dc3545

The 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 ¬x7 at level 3 and try x7 instead. If that also leads to conflict, go back to level 2 and try ¬x8. 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 c2, which involves ¬x1, ¬x2, and x3. Look at where those came from. x2 was forced by c1 because of x1 and x4. x3 was forced false by c3 because of x4. The real problem is the combination of x1 (decided at level 1) and x4 (implied at level 3). If both are true, you get a conflict no matter what.

The conflict clause is ¬x1¬x4: "do not set both x1 and x4 to true."

The solver adds this clause to the formula, then backtracks to level 1 (where x1 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:transparent

At level 1, x1 is still true, so the learned clause ¬x1¬x4 immediately becomes unit and forces ¬x4. 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 x8 at level 2 was irrelevant to the conflict. DPLL would have wasted time exploring both values of x8 before eventually reaching the same conclusion.

From a single conflict, the solver learned something that applies globally: x1 and x4 cannot both be true. This is not just avoiding the specific sequence of decisions that failed. Every future branch of the search where x1 and x4 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 G=(V,E) has:

Two kinds of vertices:

The clause that forced an implied literal is called its antecedent. For example, look at how ¬x5 entered the assignment. The clause c5=¬x5x7 had x7 assigned false, leaving ¬x5 as the only unassigned literal. BCP forced ¬x5. So c5 is the antecedent of ¬x5, and the implication graph has an edge from ¬x7 to ¬x5 labeled c5.

Similarly, ¬x6 has two incoming edges (from ¬x7 and x8, both labeled c6) because c6=¬x6x7¬x8 needed both ¬x7 and x8 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 x1@1 (true), x8@2 (true), and ¬x7@3 (false, i.e., x7 is false). Negate each: ¬x1, ¬x8, x7. The conflict clause is:

¬x1¬x8x7

This says: "at least one of these must hold: x1 is false, x8 is false, or x7 is true." It is a valid conflict clause, but it involves all three decisions, including x8, 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:#dc3545

Cut 2 (tighter): cut between x4@3 and the nodes to its right (x2, ¬x3, κ). The nodes on the reason side with edges crossing are x1@1 (true) and x4@3 (true). Negate each: ¬x1, ¬x4. The conflict clause is:

¬x1¬x4
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:#dc3545

Just two literals instead of three. This clause says: "do not set both x1 and x4 to true." It applies no matter what x7 or x8 are.

A shorter conflict clause is better. It rules out a larger region of the search space. The two-literal clause ¬x1¬x4 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 x1 is true AND x8 is true AND x7 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 ¬x7. The first UIP is x4@3: every path from ¬x7 to κ passes through x4. Cutting after x4 gives the conflict clause ¬x1¬x4.

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:

(¬x1x2x3)and(¬x1¬x2¬x4)

The variable x2 appears positive in the first clause and negative in the second. We can resolve on x2: remove x2 from the first clause and ¬x2 from the second, and combine what remains:

(¬x1x3¬x4)

The general rule:

(a1anβ)(b1bm¬β)(a1anb1bm)

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 ¬x1 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 c. After backtracking to this level, the clause c has exactly one unassigned literal (the first UIP), so it becomes unit and BCP immediately propagates it. If c 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: c=antecedent(κ)=c2=¬x1¬x2x3

Three literals: ¬x1@1, ¬x2@3, x3@3. Two literals at level 3. Not done yet.

Step 1: The last assigned literal at level 3 in c is x2. Its antecedent is c1=¬x1x2¬x4. Resolve c with c1 on x2:

¬x1¬x2x3¬x1x2¬x4¬x1x3¬x4

Now c=¬x1x3¬x4. Literals: ¬x1@1, x3@3, ¬x4@3. Still two at level 3.

Step 2: The last assigned literal at level 3 in c is x3. Its antecedent is c3=¬x3¬x4. Resolve c with c3 on x3:

¬x1x3¬x4¬x3¬x4¬x1¬x4

Now c=¬x1¬x4. Literals: ¬x1@1, ¬x4@3. One literal at level 3. Done.

Result: conflict clause ¬x1¬x4, backtrack level = 1 (the level of ¬x1, the second-highest level in c).

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:#dc3545

Work backward from the edges. If there is an edge from x1 to x2 labeled c1, then c1 must contain ¬x1 (which was falsified, making the clause unit) and x2 (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 ¬x5 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:#dc3545
Answer 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:

For example, suppose the solver has five variables and learns the conflict clause ¬x1¬x4. Before learning it, the scores might look like:

Literal Score before Score after
x1 3 3
¬x1 2 3
x4 4 4
¬x4 1 2
x2 3 3

The literals ¬x1 and ¬x4 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 (x1¬x3x5¬x7). We pick two unassigned literals to watch, say x1 and x5. As long as both are unassigned, this clause cannot be unit (it has at least two unassigned literals), so we ignore it. If x5 gets assigned false, we look at the clause and find another unassigned literal to replace it, say ¬x7. Now we watch x1 and ¬x7. 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.