Watched literals
Watched literals are the scheme by which conflict-driven SAT solvers find the clauses that unit propagation must act on[1]. A clause matters to propagation only when all but one of its literals are false, which makes it unit, or all are false, which is a conflict. As long as two of its literals are not false, neither can happen. So each clause keeps two watched literals, not false under the current assignment, and is listed in the watch list of each. When a literal becomes false, the solver visits only the clauses watching it; each looks for another non-false literal to watch instead, and only if there is none does the clause become unit on its other watch or, if that is false too, a conflict. The scheme was introduced by the Chaff solver in 2001[1], refining the head and tail pointers of the SATO solver[2], and is used by essentially every CDCL solver.
The invariant
For a clause of at least two literals, with watches , the scheme maintains, after propagation has finished,
A clause whose watches are both non-false can have no unit or conflict status, however many of its other literals are false. When becomes false, the clause is visited. If is true, the clause is satisfied and nothing needs to change. Otherwise the solver scans the other literals for one that is not false; if it finds one, it watches it instead of . If it finds none, all literals except are false: the clause is unit and is assigned, or is false as well and the clause is a conflict[1].
Why backtracking is free
Backtracking unassigns literals, which can only turn false literals into unassigned ones. A watched literal that was not false stays not false, so every clause still satisfies the invariant with the same watches, and nothing needs to be updated. A solver that keeps counters of true and false literals per clause, the scheme used before Chaff, must update every clause containing a variable both when the variable is assigned and when it is unassigned[6]. Watching also reduces the work on assignment: making false visits only the clauses currently watching , not all clauses containing it, and making true visits nothing at all.
A counter-based engine and a watched-literal engine behind the same interface, each counting clause visits, with a DPLL search on top.
(* Two propagation engines behind one interface, counting how manyclause visits each makes when literals are assigned and when they areunassigned on backtracking. Literals are non-zero integers. *)type counts = { mutable on_assign : int; mutable on_backtrack : int }module type ENGINE = sigtype tval create : int -> int array array -> counts -> tval assign : t -> int -> bool (* false on conflict *)val unassign_to : t -> int -> unit (* backtrack to a trail length *)val trail_length : t -> intval value : t -> int -> intendlet idx l = if l > 0 then 2 * l else (2 * -l) + 1(* Counters: every clause knows how many of its literals are true and howmany false, so every assignment updates every clause the variableoccurs in, and backtracking must undo each update. *)module Counters : ENGINE = structtype t = { cls : int array array; occ : int list array; tru : int array; fls : int array;value : int array; trail : int Stack.t; c : counts }let create n cls c =let occ = Array.make ((2 * n) + 2) [] inArray.iteri (fun i cl -> Array.iter (fun l -> occ.(idx l) <- i :: occ.(idx l)) cl) cls;{ cls; occ; tru = Array.make (Array.length cls) 0; fls = Array.make (Array.length cls) 0;value = Array.make (n + 1) 0; trail = Stack.create (); c }let value s l = let v = s.value.(abs l) in if l > 0 then v else -vlet trail_length s = Stack.length s.traillet rec assign s l =if value s l = 1 then true else if value s l = -1 then false else begins.value.(abs l) <- (if l > 0 then 1 else -1); Stack.push l s.trail;List.iter (fun i -> s.c.on_assign <- s.c.on_assign + 1; s.tru.(i) <- s.tru.(i) + 1) s.occ.(idx l);let ok = ref true and units = ref [] inList.iter (fun i ->s.c.on_assign <- s.c.on_assign + 1;s.fls.(i) <- s.fls.(i) + 1;if s.tru.(i) = 0 then beginlet len = Array.length s.cls.(i) inif s.fls.(i) = len then ok := falseelse if s.fls.(i) = len - 1 thenunits := Array.to_list s.cls.(i) |> List.filter (fun x -> value s x = 0) |> List.hd |> fun u -> u :: !unitsend) s.occ.(idx (-l));!ok && List.for_all (fun u -> assign s u) !unitsendlet unassign_to s len =while Stack.length s.trail > len dolet l = Stack.pop s.trail ins.value.(abs l) <- 0;List.iter (fun i -> s.c.on_backtrack <- s.c.on_backtrack + 1; s.tru.(i) <- s.tru.(i) - 1) s.occ.(idx l);List.iter (fun i -> s.c.on_backtrack <- s.c.on_backtrack + 1; s.fls.(i) <- s.fls.(i) - 1) s.occ.(idx (-l))doneend(* Two watched literals: positions 0 and 1 of each clause. Only clauseswatching a literal that has just become false are visited, andbacktracking visits nothing. *)module Watched : ENGINE = structtype t = { cls : int array array; watches : int list array; value : int array;trail : int array; mutable len : int; c : counts }let create n cls c =let watches = Array.make ((2 * n) + 2) [] inArray.iteri (fun i cl -> watches.(idx cl.(0)) <- i :: watches.(idx cl.(0));watches.(idx cl.(1)) <- i :: watches.(idx cl.(1))) cls;{ cls; watches; value = Array.make (n + 1) 0; trail = Array.make (n + 1) 0; len = 0; c }let value s l = let v = s.value.(abs l) in if l > 0 then v else -vlet trail_length s = s.lenlet assign s l =if value s l = 1 then true else if value s l = -1 then false else beginlet start = s.len ins.value.(abs l) <- (if l > 0 then 1 else -1); s.trail.(s.len) <- l; s.len <- s.len + 1;let q = ref start and ok = ref true inwhile !ok && !q < s.len dolet f = - s.trail.(!q) in incr q;let pending = ref s.watches.(idx f) ins.watches.(idx f) <- [];while !pending <> [] dolet i = List.hd !pending in pending := List.tl !pending;s.c.on_assign <- s.c.on_assign + 1;let c = s.cls.(i) inif c.(0) = f then (c.(0) <- c.(1); c.(1) <- f);let keep () = s.watches.(idx f) <- i :: s.watches.(idx f) inif value s c.(0) = 1 then keep () (* satisfied: leave it *)else beginlet k = ref 2 inwhile !k < Array.length c && value s c.(!k) = -1 do incr k done;if !k < Array.length c then begin (* move the watch *)c.(1) <- c.(!k); c.(!k) <- f; s.watches.(idx c.(1)) <- i :: s.watches.(idx c.(1))end else beginkeep ();if value s c.(0) = -1 then (ok := false; s.watches.(idx f) <- !pending @ s.watches.(idx f); pending := [])else begin (* unit: c.(0) is forced *)s.value.(abs c.(0)) <- (if c.(0) > 0 then 1 else -1);s.trail.(s.len) <- c.(0); s.len <- s.len + 1endendenddonedone;!okendlet unassign_to s len =for j = s.len - 1 downto len do s.value.(abs s.trail.(j)) <- 0 done;s.len <- lenend(* The same DPLL search on top of either engine. *)module Dpll (E : ENGINE) = structlet solve n cls =let c = { on_assign = 0; on_backtrack = 0 } inlet e = E.create n cls c inlet rec search () =let rec first v = if v > n then 0 else if E.value e v = 0 then v else first (v + 1) inlet v = first 1 inv = 0 || beginlet mark = E.trail_length e in(E.assign e v && search ()) || (E.unassign_to e mark; E.assign e (-v) && search ())|| (E.unassign_to e mark; false)end inlet r = search () in(r, c)end
Running the same search with each engine on random 3-SAT formulas at the satisfiability threshold.
open Enginesmodule C = Dpll (Counters)module W = Dpll (Watched)
Clause visits made while assigning and while backtracking.
n result counters: assign backtrack watched: assign backtrack40 SAT 10675 10165 2836 060 UNSAT 86361 86361 21104 080 SAT 621647 620627 151030 0100 UNSAT 12551837 12551837 2564924 0
Both engines explore the same search tree. The counter scheme spends as much work undoing assignments as making them, and in total seven to ten times as many clause visits as the watched scheme, which does nothing on backtracking. The gap grows with clause length: learned clauses in CDCL solvers often have dozens or hundreds of literals, of which the watched scheme touches only the two watches unless they are hit[1][6].
Implementation
Most implementations keep the two watched literals in the first two positions of the clause, which is what the code above does; moving a watch swaps the new literal into position and pushes the clause onto the new literal's watch list[3]. Several refinements are standard.
Blocking literals
Visiting a clause means loading it from memory, usually a cache miss. Chu, Harwood and Stuckey store with each watch-list entry one other literal of the clause, the blocking literal; if it is true, the clause is satisfied and need not be loaded at all[4]. MiniSat and its successors adopted the technique.
Binary clauses
A clause of two literals is its own pair of watches and never moves them. Solvers therefore keep binary clauses in separate implication lists: when a literal becomes false, the other literal of each binary clause containing it is simply assigned[7].
Where to look for a replacement
The scan for a new watch can start from the beginning of the clause or from where the last scan stopped. Gent analyzed the choices and showed that resuming the scan where the previous one stopped, wrapping around the clause, gives the best worst-case cost[5].
Other uses
The idea applies wherever a constraint can only propagate once all but a few of its variables are fixed. Constraint solvers such as Minion watch variables of general constraints[9], and SMT solvers watch the atoms of theory lemmas in the same way as clauses. Pseudo-Boolean solvers generalize it to linear inequalities over Boolean variables, watching enough literals that their weights could still satisfy the constraint.
History
Early DPLL implementations and GRASP detected unit clauses with counters of assigned literals per clause[8][6]. Zhang's SATO of 1997 used head and tail pointers, which move inward from the ends of a clause as literals become false, and must be moved back on backtracking[2]. The Chaff solver of Moskewicz, Madigan, Zhao, Zhang and Malik, published in 2001, replaced them with two watches that can be anywhere in the clause and need no restoration on backtracking; together with VSIDS it made Chaff an order of magnitude faster than earlier solvers[1]. MiniSat's compact implementation of 2003 made the scheme standard[3], and blocking literals were added in 2008[4].
see also
- Unit propagationUnit propagation is the inference rule at the core of SAT solvers: when every literal of a clause is false under the current partial assignment except one unassigned literal, that literal must be true. Applying the rule until nothing changes either extends the assignment or finds a clause with every literal false, a conflict. It decides satisfiability of Horn formulas on its own, it takes most of the running time of a modern CDCL solver, and it is implemented with two watched literals per clause so that only clauses that may have become unit are ever inspected.
- CDCLConflict-driven clause learning (CDCL) is the algorithm of modern SAT solvers. It extends DPLL backtracking search with three ideas: when propagation falsifies a clause, conflict analysis derives a new clause that explains the conflict and adds it to the formula; the solver then backjumps non-chronologically to the decision level at which the new clause forces a literal; and branching follows the variables involved in recent conflicts. With watched literals, restarts and clause deletion, CDCL solvers decide industrial formulas with millions of variables, and with restarts they are as strong as general resolution, exponentially stronger than DPLL.
- VSIDSVSIDS, Variable State Independent Decaying Sum, is the branching heuristic of conflict-driven SAT solvers. Each variable has an activity score, which is increased whenever the variable takes part in a conflict and decays geometrically over time; when the solver must make a decision, it branches on the unassigned variable with the highest activity. The heuristic is cheap, because it depends on conflicts rather than on the current formula, and it focuses the search on the variables involved in recent conflicts. It was introduced by the Chaff solver in 2001 and, in the exponential form used by MiniSat, remains the default in most solvers.
- DPLLDPLL, the Davis–Putnam–Logemann–Loveland procedure, is the backtracking search algorithm for Boolean satisfiability on which all complete SAT solvers are built. It repeatedly simplifies a CNF formula by unit propagation and pure literal elimination, then chooses an unassigned variable and tries both of its values in turn, backtracking to the most recent choice when a clause becomes false. It is sound and complete, uses memory linear in the formula, and its runs on unsatisfiable formulas correspond to tree-like resolution proofs, which gives exponential lower bounds such as the n! − 1 decisions it needs for the pigeonhole formulas. Clause learning turned it into CDCL.
further reading
- [1]M. W. Moskewicz, C. F. Madigan, Y. Zhao, L. Zhang, S. Malik, “Chaff: engineering an efficient SAT solver”, DAC (2001).
- [2]H. Zhang, “SATO: an efficient propositional prover”, CADE, Lecture Notes in Computer Science 1249 (1997).
- [3]N. Eén, N. Sörensson, “An extensible SAT-solver”, SAT, Lecture Notes in Computer Science 2919 (2004).
- [4]G. Chu, A. Harwood, P. J. Stuckey, “Cache conscious data structures for Boolean satisfiability solvers”, Journal on Satisfiability, Boolean Modeling and Computation 6 (2009).
- [5]I. P. Gent, “Optimal implementation of watched literals and more general techniques”, Journal of Artificial Intelligence Research 48 (2013).
- [6]I. Lynce, J. Marques-Silva, “Efficient data structures for backtrack search SAT solvers”, Annals of Mathematics and Artificial Intelligence 43 (2005).
- [7]A. Biere, “PicoSAT essentials”, Journal on Satisfiability, Boolean Modeling and Computation 4 (2008).
- [8]J. P. Marques-Silva, K. A. Sakallah, “GRASP: a search algorithm for propositional satisfiability”, IEEE Transactions on Computers 48 (1999).
- [9]I. P. Gent, C. Jefferson, I. Miguel, “Watched literals for constraint propagation in Minion”, CP, Lecture Notes in Computer Science 4204 (2006).
last updated