Satisfiability Problem · SAT solvers
Lesson 5
Our Boolean variables will be as follows: for \(1 \le i, j, k \le 9\) the variable \(x_{ijk}\) will indicate whether we place the digit \(k\) in cell \((i,j)\) or not (we number cells from the top left corner). The SAT-solver needs variables with a single (not triple) index, so we again have a converter \(\texttt{varnum}\) that maps triples of indices to a unique index from 1 to 999.
Each clause of the CNF formula specifies a constraint of the form: at least one of the given literals must equal one. We will also need constraints of the form: exactly one of the given literals equals one. We write a special function \(\texttt{exactly\_{one}\_{of}}\) for this. It takes as input a set of literals \(\{l_{1}, \dotsc, l_{k}\}\) and writes constraints stating that at least one of these literals holds and no two can hold simultaneously: \[(l_{1} \lor l_{2} \lor \dotsb \lor l_{k}) \land \bigwedge_{1 \le i < j \le k}(\overline{l_i}\lor \overline{l_j})\]
from itertools import combinations, product
import pycosat
def varnum(row, column, digit):
return 100 * row + 10 * column + digit
def exactly_one_of(literals):
clauses = [[l for l in literals]]
for pair in combinations(literals, 2):
clauses.append([-l for l in pair])
return clauses
def one_digit_in_every_cell():
clauses = []
for row, column in product(range(1, 10), repeat=2):
clauses += exactly_one_of([varnum(row, column, digit) for digit in range(1, 10)])
return clauses
def one_digit_in_every_row():
clauses = []
for row, digit in product(range(1, 10), repeat=2):
clauses += exactly_one_of([varnum(row, column, digit) for column in range(1, 10)])
return clauses
def one_digit_in_every_column():
clauses = []
for column, digit in product(range(1, 10), repeat=2):
clauses += exactly_one_of([varnum(row, column, digit) for row in range(1, 10)])
return clauses
def one_digit_in_every_block():
clauses = []
for row, column in product([1, 4, 7], repeat=2):
for digit in range(1, 10):
clauses += exactly_one_of([varnum(row + a, column + b, digit) for (a, b) in product(range(3), repeat=2)])
return clauses
def solve_puzzle(puzzle):
assert len(puzzle) == 9
assert all(len(row) == 9 for row in puzzle)
clauses = []
clauses += one_digit_in_every_cell()
clauses += one_digit_in_every_row()
clauses += one_digit_in_every_column()
clauses += one_digit_in_every_block()
for (row, column) in product(range(1, 10), repeat=2):
if puzzle[row - 1][column - 1] != "*":
digit = int(puzzle[row - 1][column - 1])
assert digit in range(1, 10)
clauses += [[varnum(row, column, digit)]]
solution = pycosat.solve(clauses)
if isinstance(solution, str):
print("No solution")
exit()
assert isinstance(solution, list)
for row, column, digit in product(range(1, 10), repeat=3):
if varnum(row, column, digit) in solution:
print(digit, end="")
if column == 9 and digit == 9:
print('')
difficult_puzzle = [
"8********",
"**36*****",
"*7**9*2**",
"*5***7***",
"****457**",
"***1***3*",
"**1****68",
"**85***1*",
"*9****4**"
]
solve_puzzle(difficult_puzzle)812753649
943682175
675491283
154237896
369845721
287169534
521974368
438526917
796318452