12 Solving SAT and SMT Problems Using Z3
You will also need to install the Z3 SMT solver and its Python bindings. Please follow the instruction at Insatll Z3 to install Z3 solver.
Now to check that everything was correctly installed, start a Python session and run:
* from z3 import
12.1 Review of Logical Operators
And(x, y) is true iff x and y are true.
Or(x, y) is true iff x or y are true.
Not(x) is true iff x is false.
Implies(x, y) (x ==> y) is true iff x is true implies that y is true.
It is common to write truth tables to describe the behavior of these operators.
We show the tables for And, Or, and Implies below.
And | Or | Implies | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|
12.2 SAT & SMT
Suppose we want to find a satisfying assignment for the expression (a || !b) && (!a || c). That means we’re looking for values of a, b, and c that make the entire expression evaluate to true.
One way to approach this is by checking all possible combinations of truth values for a, b, and c. Since each variable can be either true or false, there are 2^3=8 possible combinations.
Can you list all 8 combinations of a, b, and c?
In general, for n variables, there will be 2^n possible assignments, so the approach of "trying everything" is not efficient for large n. In fact, this is the SAT ("satisfiability") problem, which is NP-complete, meaning that it is "one of the hardest problems to which solutions can be verified quickly. But this difficult problem is exactly what tools like the Z3 SMT solver! Here’s how we can solve the problem using the Z3:
*
from z3 import = Bool('a')
a = Bool('b')
b = Bool('c')
c
= Solver()
s .add(Or(a, Not(b))) # a || !b
s.add(Or(Not(a), c)) # !a || c
s.check() # do these equations have a solution?
s.model() # get the solution
s
# Output: [b = False, a = False, c = False]
The output says that the formula (a || !b) && (!a || c) will be true when a, b, and c are all false.
But what if we want a different solution?
In this case, we can add an additional constraint that says that we don’t want the solution where a, b, and c are all false.
.add(Not(And(Not(a), Not(b), Not(c))))
s.check()
s.model()
s
# Output: [b = False, c = True]
This output says that the formula (a || !b) && (!a || c) will be true when b is false and c is true (it doesn’t matter what a is).
Let’s try adding one more constraint:
.add(And(Not(a), b))
s.check()
s
# Output: unsat
The output unsat means unsatisfiable. The solver is telling us that there is no possible choice of a, b, c that makes the current constraints true.
Z3 can find solutions to more than just SAT problems – it is an SMT solver.
SMT stands for SAT modulo theories; it generalizes SAT to formulas involving integers, strings, arrays, and so on. For example, we can use Z3 to find a solution for the system of equations 3x - 2y = 1, y - x = 1.
from z3 import *
x, y = Ints('x y')3 * x - 2 * y == 1, y - x == 1)
solve(
3, y = 4] # Output: [x =
solve is shorthand for the check and model we were using above.
12.3 Solving Puzzles with SMT
SMT solvers are useful for more than finding solutions to a random set of equations – you just have to know how to encode your problem in a form that the SMT solver understands. Here are three examples of puzzles that an SMT solver can solve. Next time you’re working on a puzzle, ask yourself whether it could be solved using SMT.
Often, SMT solvers are not used to find a solution, but to *prove that no solution exists*. For instance, say that we want to prove the mathematical fact y > 0 ==> x + y > x. To do this, we can ask the SMT solver if it ever the case that this is not true.
x, y = Ints('x y')0, x + y > x)))
solve(Not(Implies(y >
# Output: no solution
The output here says that it is never not the case that y > 0 ==> x + y > x (a double negative). In other words: this formula is true for any choice of x and y!
12.4 Rabbits and Chickens
Uncle Henry has 48 rabbits and chickens. He knows his rabbits and chickens have 108 legs, but does not know the exact number of rabbits and chickens. Can you help him? How many rabbits and chickens does Uncle Henry have?
#Create 2 integer variables
rabbit, chicken = Ints('rabbit chicken')
s = Solver()48) # 48 animals
s.add(rabbit + chicken == 4 * rabbit + 2 * chicken == 108) # 108 legs
s.add(
s.check() # solve the constraints s.model() # get the solution
12.5 Cats and Dogs
Say that you want to spend exactly 100 dollars to buy exactly 100 animals. Dogs cost 15 dollars, cats cost 1 dollar, and mice cost 25 cents each. You have to buy at least one of each. How many of each should you buy? Solution from
3 integer variables
# Create
dog, cat, mouse = Ints('dog cat mouse')
1, # at least one dog
solve(dog >= 1, # at least one cat
cat >= 1, # at least one mouse
mouse >= 100, # 100 animals total
dog + cat + mouse == 100 dollars (10000 cents):
# We have 15 dollars (1500 cents),
# dogs cost 1 dollar (100 cents), and
# cats cost 25 cents
# mice cost 1500 * dog + 100 * cat + 25 * mouse == 10000)
12.6 Sudoku
Sudoku is a puzzle where the goal is to insert numbers in boxes to satisfy the following condition: each row, column, and 3x3 box must contain the digits 1 through 9 exactly once.
The solution for the puzzle shown below. You can run it with ‘python3 sudoku.py‘.
from z3 import *
9x9 matrix of integer variables
# "x_%s_%s" % (i+1, j+1)) for j in range(9) ]
X = [ [ Int(for i in range(9) ]
in {1, ..., 9}
# each cell contains a value 1 <= X[i][j], X[i][j] <= 9)
cells_c = [ And(for i in range(9) for j in range(9) ]
# each row contains a digit at most oncefor i in range(9) ]
rows_c = [ Distinct(X[i])
# each column contains a digit at most oncefor i in range(9) ])
cols_c = [ Distinct([ X[i][j] for j in range(9) ]
3x3 square contains a digit at most once
# each 3*i0 + i][3*j0 + j]
sq_c = [ Distinct([ X[for i in range(3) for j in range(3) ])
for i0 in range(3) for j0 in range(3) ]
sudoku_c = cells_c + rows_c + cols_c + sq_c
'0' for empty cells
# sudoku instance, we use 0,0,0,0,9,4,0,3,0),
instance = ((0,0,0,5,1,0,0,0,7),
(0,8,9,0,0,0,0,4,0),
(0,0,0,0,0,0,2,0,8),
(0,6,0,2,0,1,0,5,0),
(1,0,2,0,0,0,0,0,0),
(0,7,0,0,0,0,5,2,0),
(9,0,0,0,6,5,0,0,0),
(0,4,0,9,7,0,0,0,0))
(
0,
instance_c = [ If(instance[i][j] ==
True,
X[i][j] == instance[i][j])for i in range(9) for j in range(9) ]
s = Solver()
s.add(sudoku_c + instance_c)if s.check() == sat:
m = s.model()for j in range(9) ]
r = [ [ m.evaluate(X[i][j]) for i in range(9) ]
print_matrix(r)else:
print("failed to solve")
12.7 Sum of Two Real Numers
The sum of two nonzero real numbers is 4 times their product. What is the sum of the reciprocals of the two numbers? We’re going to solve this over real numbers:
from z3 import *
x, y = Reals('x y')
s=Solver() 0)
s.add(x > 0)
s.add(y > 4 * x * y)
s.add(x + y == print (s.check())
m=s.model()print ("the model:", m)
print ("the answer:",m.evaluate (1/x + 1/y))
1/x + 1/y = (x+y)/ (x * y) == 4
# #Instead of pulling values from the model and then
# compute the final result on Pythonβs side,
# we can evaluate an expression ( x1 + y1 ) inside the model weβve got:#sat
#the model:
1, y = 1/3]
#[x = #the answer: 4
12.8 Coin Sum
Euler 31: “Coin sums” problem is as follows: In the United Kingdom the currency is made up of pound (£) and pence (p). There are eight coins in general circulation: 1p, 2p, 5p, 10p, 20p, 50p, £1 (100p), and £2 (200p). It is possible to make £2 in the following way:
1ΓΒ£1 + 1Γ50p + 2Γ20p + 1Γ5p + 1Γ2p + 3Γ1p
from z3 import *
a,b,c,d,e,f,g,h = Ints('a b c d e f g h')
s = Solver()1*a + 2*b + 5*c + 10*d + 20*e + 50*f + 100*g + 200*h == 200,
s.add(0, b>=0, c>=0, d>=0, e>=0, f>=0, g>=0, h>=0)
a>=
result =[]while True:
if s.check() == sat:
m = s.model()print (m)
result.append(m)new constraint the blocks the current model
# Create a
block = []for d in m:
# create a constant from declaration
c=d()
block.append(c != m[d])
s.add(Or (block))else:
print (len(result))
break
12.9 8 Queens Puzzle
The 8-Queens problem asks: given an 8×8 chessboard, place 8 queens
on the board so that no two queens attack each other.
from z3 import *8
N =
var(row, col):
def
return Int(f'cell_{row}_{col}')
var(row, col) for col in range(N) ] for row in range(N) ]
vs = [ [
cells_defined = [0 <= vs[i][j], vs[i][j] <= 1)
And(for i in range(N) for j in range(N)
]
s = Solver()
s.add(cells_defined)1 for i in range(N)]
rows_ok = [Sum(vs[i]) ==
# all entries within a column must be distinctfor i in range(N)]) == 1 for j in range(N) ]
cols_ok = [Sum([vs[i][j]
1, vs[k][h] == 1,
diagonals_ok = [Implies( And(vs[i][j] == abs(k - i) != abs(j - h))
i != k, j != h), for i in range(N) for j in range(N)
for k in range(N) for h in range(N)]
s.add(rows_ok)
s.add(cols_ok)
s.add(diagonals_ok)if s.check() == sat:
m = s.model()for i in range(N):
for j in range(N):
if m[var(i,j)] == 1:
print(m[var(i,j)],end ="|")
else:
print(" ",end ="|")
print("
---------------")
else:
print("unsat")
12.10 Circle, Square, and Triangle
# circle + circle = 10
# circle * square + sqaure = 12
# circle * square - triangle * circle = circle
# What is th evalue of the triangle?
#!/ usr/bin/ python
from z3 import *
circle , square , triangle = Ints('circle square triangle')
s = Solver()10)
s.add( circle + circle ==12)
s.add( circle * square + square ==
s.add( circle *square - triangle * circle == circle )if s.check() == unsat:
print("UNSAT")
else:
print (s.model())
12.11 De Morgan’s laws
We want to prove
We will prove it by showing there is no falsifying interpertation of the formula.
from z3 import *
p, q = Bools('p q')
demorgan = And(p, q) == Not(Or(Not(p), Not(q)))print (demorgan)
def prove(f):
s = Solver()
s.add(Not(f))if s.check() == unsat:
print ("proved")
else:
print ("failed to prove")
print ("Proving demorgan...")
prove(demorgan)
12.12 Integer Overflow
When a 16-bit vector is used, any integer greater than 2^16 - 1 = 65,535 will cause BV2Int to overflow. In the example below, if the sum of x and y exceeds 65,535, then BV2Int(x) + BV2Int(y) will not be equal to BV2Int(x + y).
from z3 import *'x', 16)
x = BitVec('y', 16)
y = BitVec(
s = Solver()
s.add((BV2Int(x) + BV2Int(y)) != BV2Int(x+y))if s.check()== sat:
print(s.model())
else:
print("unsat")
1 != BV2Int(x+1)) solve(BV2Int(x)+
12.13 Wedding Seating
Alice does not sit next to Charlie
Alice does not sit on the leftmost chair
Bob does not sit to the right of Charlie
Each person gets a chair
Every person gets at most one chair
Every chair gets at most one person
We will model assignment via nine boolean variables xpc, meaning the person p sits in chair c. Here are the constraints:
xal or xar implies not xcm: if Alice sits on the left or right, Charlie cannot sit in the middle.
xam implies not xcl and xcr: if Alice sits on the middle, Charlie cannot sit in the left or right.
not xal: Alice cannot sit on the leftmost chair
xcl ==> not xbm: If Charlie sits on left, Bob cannot sit in middle
xcm ==> not xbr: If Charlie sits on middle, Bob cannot sit in right
from z3 import *
xal, xam, xar, xbl, xbm, xbr, xcl, xcm, xcr =
Bools('xal xam xar xbl xbm xbr xcl xcm xcr')
s = Solver()not sit next to Charlie
# Alice does
s.add(And(Implies(Or(xal, xar), Not(xcm) ),
Implies(xam, And(Not(xcl), Not(xcr)))))not sit on the leftmost chair
# Alice does
s.add(Not(xal) )not sit to the right of Charlie
# Bob does #s.add(And( Implies(xcl, Not(xbm)), Implies(xcm, Not(xbr))))
#Each person gets a chair
s.add( Or( xal, xam, xar) )
s.add( Or( xbl, xbm, xbr) )
s.add( Or( xcl, xcm, xcr) )
#Every person gets at most one chair
s.add( Or( Not(xal), Not(xam) ) )
s.add( Or( Not(xal), Not(xar) ) )
s.add( Or( Not(xam), Not(xar) ) )
#analogous for persons b and c
s.add( Or( Not(xbl), Not(xbm) ) )
s.add( Or( Not(xbl), Not(xbr) ) )
s.add( Or( Not(xbm), Not(xbr) ) )
s.add( Or( Not(xcl), Not(xcm) ) )
s.add( Or( Not(xcl), Not(xcr) ) )
s.add( Or( Not(xcm), Not(xcr) ) )
# Every chair gets at most one person
s.add( Or( Not(xal), Not(xbl) ) )
s.add( Or( Not(xal), Not(xcl) ) )
s.add( Or( Not(xbl), Not(xcl) ) )for chairs m and r
# analogous
s.add( Or( Not(xam), Not(xbm) ) )
s.add( Or( Not(xam), Not(xcm) ) )
s.add( Or( Not(xbm), Not(xcm) ) )
s.add( Or( Not(xar), Not(xbr) ) )
s.add( Or( Not(xar), Not(xcr) ) )
s.add( Or( Not(xbr), Not(xcr) ) )
if s.check() == sat:
print(s.model())
else:
print("UNSAT")