Pentominoes,
Exact Covers, and Algorithm X
David Radcliffe
February 9, 2017
A pentomino is a shape formed by joining five equal squares along their edges. There are 12 different pentominoes.
http://donsteward.blogspot.com/2012/04/pentominoes.html
Pentomino Puzzles
How many ways are there to tile an 8x8 square with all 12 pentominoes, if the corners are removed?
(Answer: 2170)
Tiling a clipped square
The Exact Cover Problem
Input: A matrix of zeros and ones.
Output: A set of rows such that every column adds to 1.
Algorithm X
- If the matrix A has no columns, terminate successfully.
- Choose a column c with minimal sum.
- For each row r having a 1 in column c,
- Include r in the partial solution,
- Delete all rows that intersect row r,
- Delete all columns have a 1 in row r,
- Repeat the algorithm recursively on the reduced matrix.
Algorithm X Example
1. Select column c with smallest sum.
2. Select a row r with 1 in column c.
3. Delete rows that overlap with row r.
4. Delete columns with 1 in row r.
def exact_cover(A):
# If matrix has no columns, terminate successfully.
if A.shape[1] == 0:
yield []
else:
# Choose a column c with the fewest 1s.
c = A.sum(axis=0).argmin()
# For each row r such that A[r,c] = 1,
for r in A.index[A[c] == 1]:
B = A
# For each column j such that A[r,j] = 1,
for j in A.columns[A.loc[r] == 1]:
# Delete each row i such that A[i,j] = 1
B = B[B[j] == 0]
# then delete column j.
del B[j]
for partial_solution in exact_cover(B):
# Include r in the partial solution.
yield [r] + partial_solution
Python implementation of Algorithm X
Implementing a Pentomino Solver
- A pentomino puzzle can be represented as an instance of the exact cover problem.
- Each row in the matrix represents a possible location for a pentomino.
- One column for each square to be covered.
- One column for each type of pentomino.
- Dimensions: 1915 × 72.
The Matrix
The first 49 rows of the input matrix.
import pandas as pd
import numpy as np
from knuth import exact_cover
pentominoes = [
np.array(p) for p in [
# F
[[0,1,1],
[1,1,0],
[0,1,0]],
# I
[[1,1,1,1,1]],
# L
[[1,1,1,1],
[0,0,0,1]],
# N
[[1,1,0,0],
[0,1,1,1]],
# P
[[1,1,1],
[0,1,1]],
# T
[[1,1,1],
[0,1,0],
[0,1,0]],
# U
[[1,0,1],
[1,1,1]],
# V
[[1,0,0],
[1,0,0],
[1,1,1]],
# W
[[1,0,0],
[1,1,0],
[0,1,1]],
# X
[[0,1,0],
[1,1,1],
[0,1,0]],
# Y
[[0,0,1,0],
[1,1,1,1]],
# Z
[[1,1,0],
[0,1,0],
[0,1,1]]
]
]
def all_orientations(A, i):
"""Generate all distinct orientations of the pentominoes,
including rotations and reflections."""
# Fixing the orientation of the first (F) pentomino eliminates
# redundant solutions resulting from rotations or reflections."""
if i == 0:
yield A
return
seen = set()
# Apply transpose, left/right flip, and up/down flip in all combinations
# to generate all possible orientiations of a pentomino.
for A in (A, A.T):
for A in (A, np.fliplr(A)):
for A in (A, np.flipud(A)):
s = str(A)
if not s in seen:
yield A
seen.add(s)
def all_positions(A, i):
""" Find all positions to place the pentominoes. """
for A in all_orientations(A, i):
rows, cols = A.shape
for i in range(9 - rows):
for j in range(9 - cols):
M = np.zeros((8, 8), dtype='int')
M[i:i+rows, j:j+cols] = A
if M[0,0] == M[0,7] == M[7,0] == M[7,7] == 0:
yield np.delete(M.reshape(64), [0, 7, 56, 63])
rows = []
for i, P in enumerate(pentominoes):
for A in all_positions(P, i):
A = np.append(A, np.zeros(12, dtype='int'))
A[60+i] = 1
rows.append(list(A))
A = pd.DataFrame(rows)
A.to_csv('matrix.csv')
covers = np.array(list(exact_cover(A)), dtype='int')
np.savetxt('exact-covers.csv', covers, delimiter=',', fmt='%d')
Pentomino solver in Python.
Sudoku as an exact cover problem
- 729 rows (ways to write one number in the grid).
- Delete any rows that are contradicted by the hints.
- 324 columns enforce the following conditions:
- Every square is filled,
- Every number occurs in every row,
- Every number occurs in every column,
- Every number occurs in every 3x3 block.
Note that each row has 4 ones and 320 zeros.
import numpy as np
import pandas as pd
from knuth import exact_cover
p = [int(x) for x in (
"010009000"
"743002000"
"000800102"
"000000400"
"000060050"
"009001007"
"005000060"
"001000900"
"000750801"
)]
p = np.array(p).reshape((9,9))
A = np.zeros((729, 324), dtype=int)
row = 0
index = []
for i in range(9):
for j in range(9):
rng = [p[i,j]-1] if p[i,j] else range(9)
for k in rng:
A[row, 9*i + k] = 1
A[row, 81 + 9*j + k] = 1
A[row, 162 + 27*(i//3) + 9*(j//3) + k] = 1
A[row, 243 + 9*i + j] = 1
index.append("%d %d %d" % (i,j,k))
row += 1
A = pd.DataFrame(A[:row,:], index=index)
solution = next(exact_cover(A))
m = np.zeros((9,9), dtype=int)
for s in solution:
i, j, k = map(int, s.split())
m[i, j] = k + 1
print (m)
Sudoku solver in Python
Pentominoes, Exact Covers, and Algorithm X
By David Radcliffe
Pentominoes, Exact Covers, and Algorithm X
- 4,116