Chair of Computer Architecture

University Freiburg

Lecture 03

DP was not useful for computers back in the 50s. The technical report they published said you are faster on paper. The second algorithm performed much better, even on 50s machines.

img/Untitled.png

This algorithm trades space for time. Instead of eliminating variables, we do a case split and look at both cases separately.

img/Untitled%201.png

img/Untitled%202.png

Unit resolution is confluent. You can apply it arbitrarily. In the end, you end up with the same formula. The power of modern SAT solvers comes from this rule, implemented in a very sophisticated way.

img/Untitled%203.png

\[(\lnot a \vee \lnot b) \vee (a \leftrightarrow b) \\ \text{Unsat: } a \wedge b \wedge \lnot (a \leftrightarrow b) \\ a \wedge b \wedge \lnot ((\lnot a \vee b) \wedge (a \vee \lnot b)) \\ a \wedge b \wedge ((a \wedge \lnot b) \vee (\lnot a \wedge b)) \\ a \wedge b \wedge ( (a \vee (\lnot a \wedge b)) \wedge (\lnot b \vee (\lnot a \wedge b))) \\ a \wedge b \wedge (a \vee \lnot a) \wedge (a \vee b) \wedge (\lnot b \vee \lnot a) \wedge (\lnot b \vee b) \\ a \wedge b \wedge (a \vee b) \wedge (\lnot a \vee \lnot b)\]

img/Untitled%204.png

img/Untitled%205.png

BCP means binary constraint propagation.

img/Untitled%209.png

You add a literal with a unit clause whenever you call the solver. The first call forces $l$ to be true, and the second call forces it to be false. If there is one empty clause ($\bot$ symbol), the formula is unsat. If you get to satisfiable, you could abort, backtrack and return a satisfying assignment.

We are doing a recursive call but could implement this in a loop. But the first call has to be recursive. You need to return to $F$ and remember the literal you’ve chosen. BCP you want to make fast because approximately 80% of the time is spent here.

The “pick remaining variable” part previously took 80% of the time. With a trick, it got reduced to approximately 10%. You need a good decision heuristic: pick a variable $x$ and a phase to try first, i.e., positive or negative.

The pure literal rule is also in the original paper but dropped here. It is not done in modern solvers anymore.

img/Untitled%2010.png

Picking the right variable $x$ becomes irrelevant if you already know the phase you will assign it to. To find sat examples, we need a good heuristic at $l \in { x, \lnot x}$. For unsat, this does not matter.

The worst-case time complexity of the DPLL algorithm is $\mathcal{O}(2^n)$, where $n$ is the number of variables in the formula. In the worst case, the algorithm may need to explore all possible variable assignments, equivalent to traversing a binary tree of depth $n$. However, the algorithm often performs much better in practice than this worst-case scenario.

The space complexity of the DPLL algorithm is $\mathcal{O}(n)$, as it needs to store the current partial assignment in memory, which can have up to $n$ variables. The algorithm uses a depth-first search strategy, which means it only needs to store the variables in the current branch, not the entire search space.

Modern SAT solvers based on the DPLL algorithm, called Conflict-Driven Clause Learning (CDCL) solvers, have significantly improved performance compared to the basic DPLL algorithm through various optimizations and learning techniques.

Example of BCP

img/Untitled%2011.png

$a (\bar{a} \vee b) (\bar{b} \vee c) (\bar{c} \vee d)$ ⇒ in the end, you get $d$

img/Untitled%2012.png

img/Untitled%2013.png

$(\bar{a} \vee b) (\bar{b} \vee c) (\bar{c} \vee d) \bar{d}$ ⇒ in the end, you get $\bar{a}$

img/Untitled%2014.png

Can propagate from left to right and right to left

img/Untitled%2015.png

The arrows represent hyper-edge implications. The clause encodes three types of propagation. This is the reason why CNF is used in practice. Unit propagation is confluent. You can do this in any order.

img/Untitled%2016.png

img/Untitled%2017.png

This algorithm is exponential. For every recursive call, you double the formula

The shannon expansion provides a technique for systematically exploring a problem’s search space, simplifying Boolean formulae, and helping to identify satisfiability.

img/Untitled%2018.png

img/Untitled%2019.png

The $x$-axis represents CPU time, and the $y$-axis represents the number of solved instances.

Why is KISSAT considered superior to other solvers? About two years ago (as of 2021), an idea was conceived and has now been effectively implemented. This idea revolves around target phases, which originated from a Ph.D. thesis and has significantly impacted these maple solvers.

The concept involves dividing the search process into two distinct parts: the first focuses on unsatisfiable instances, while the second concentrates on satisfiable ones. The solver seamlessly switches between these phases, employing numerous restarts. In contrast, the alternative solver, known as “stable,” is notably slower due to the absence of restarts. A substantial improvement is achieved by combining target phases with a technique inspired by local search.

Although CaDiCal integrated this approach in 2019 and successfully solved many instances, it falls short compared to the maple solvers in the cvf. The KISSAT solver effectively implements these techniques. Additionally, several other lower-level factors demonstrate the practicality of these ideas. You can refer to the accurate system description provided for a more detailed understanding of why KISSAT performs exceptionally fast.

img/Untitled%2020.png

You do not put symbols, but integers in there, corresponding to the DIMACS format. We can think of this as lists. In reality, it will be stacked (vector in C++). The arrows are references to clauses. Each arrow is a reference. Every clause gives you a row. The literals are the columns. Think of this as a sparse matrix implementation. The lists on the left contain all occurrences of the literals.

img/Untitled%2021.png

img/Untitled%2022.png

BCP example

We can represent the truth value of a variable using a signed char, which requires only one byte of storage. The encoding scheme only necessitates a single byte to represent the truth value of a variable. A negative number indicates a false assignment, a positive number denotes a true assignment, and 0 signifies an unassigned variable.

img/Untitled%2023.png

Here, $X$ means “unassigned.”

img/Untitled%2023.png

We need to call DPLL on this because we first need a decision. We cannot propagate anything at first.

img/Untitled%2024.png

We increase the decision level. The decision level means how many recursive calls we have.

Initially, 0 is the height of the trail, and we push it on the control stack. Hence “control” increases in height with 0 on the top.

Why do we have 0 in the control stack in the beginning? On decision level 0, you want to remember the start of the literals on the trail with decision level 0. Then we need to pick a variable and a value.

img/Untitled%2025.png

Then we need to pick a variable and a value. Variable 1 becomes true. In C code, true would be +1, and false would be -1.

Now we put the literal 1 on the trail. Hence “1” is on the trail with the corresponding phase. If it were false, we would put -1.

Then we need to do boolean constraint propagation. In actual implementation, we would have a pointer “propagated” on the trail, which gives you the number of literals you have already propagated. Initially, this thing is 0. Now it is one literal, 1.

Now going to this matrix and going through all clauses in which negation of literal occurs ⇒ pink line here

Boolean constraint propagation will now force 2 to be true. The resolution of 1 and -1 cancels, and we are left with 2. The only way to make this clause true is to set 2 to true.

img/Untitled%2026.png

Now we are putting 2 on the stack, and the same thing continues: we go to the negative occurrences of 2, and we have to put 3 on the trail.

Then we would propagate 3, but there is no negative clause in which 3 occurs, meaning we have arrived at a “fixpoint.”

We now have 3 assignments on the trail and must make another decision. So we increase the decision level, and record the height of the trail, thus putting 3 on top of “control.”

The big lines around the trail indicate a block on decision level 1. These are all variables that were assigned on decision level 1. None is assigned on decision level 0, so there is no unit clause. You would see something on the trail if you had a unit clause.

img/Untitled%2027.png

Then we need to pick 4, and we can propagate 5

img/Untitled%2028.png

img/Untitled%2029.png

This is linear: the height of the trail is $\text{max}(\text{num_variables})$, so you can pre-allocate it when you implement the solver, just set it to the number of variables.

The same for the control. It can never be bigger than the number of variables plus 1. Beware of the off-by-one bug.

We can implement this going down the recursion, but how do we go up, i.e., backtrack? We go backward. If we had to backtrack to decision level 1, we would go through the trail until this decision level, to which we want to backtrack and erase all the assignments.

All this shows that we can do this in linear space with no need to copy the formula

Check Your Knowledge