nonograms/main.py

323 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
"This module does something"
from itertools import combinations_with_replacement
import sys
def solve1(width, pattern):
"""
This yields a tuple for each possible layout of
pattern inside the row. The tuple elements are the
gaps before each block in pattern.
The tuple doesn't include the last gap, since that's
just: width - sum(sol) - sum(pattern)
"""
spaces = width - (sum(pattern) + len(pattern) - 1)
for sol in combinations_with_replacement(range(spaces + 1), len(pattern)):
sol = sol[0:1] + tuple((sol[i] - sol[i-1] + 1) for i in range(1, len(sol)))
yield sol
def expand_solution(solution, width, pattern):
"""
expands a solution to a tuple of 1 (ON) and 0 (OFF)
"""
r = []
for s, p in zip(solution, pattern):
r.extend([0] * s)
r.extend([1] * p)
r.extend([0] * (width - sum(solution) - sum(pattern)))
return tuple(r)
def matches(expanded_solution, constraints):
"""
solution is a tuple of spaces, the output of solve1
constraints is a tuple of values from 1, 0 and -1, that
mean:
0 -> OFF
1 -> ON
-1 -> not constrained
"""
for s, c in zip(expanded_solution, constraints):
if c == -1:
continue
if c != s:
return False
return True
def solve2(width, pattern, constraints=None):
"""
@width: int
@pattern: sequence of ints
@constraints: optional list of length width containing 1,0,-1 as elements
Does the same as solve1, but takes constraints
in consideration to be faster than solve1 + matches
"""
if len(pattern) == 0:
return tuple()
if constraints is None:
constraints = [-1] * width
p = pattern[0]
# the first gap can go from 0 to the following, inclusive
maxgap = width - sum(pattern[1:]) - (len(pattern) - 1) - p
for gap in range(maxgap + 1):
# constraint width
cw = (gap + p + 1) if len(pattern) > 1 else width
e = expand_solution((gap,), cw, (p,))
if not matches(e, constraints[:cw]):
continue
if len(pattern) == 1:
yield (gap,)
continue
subwidth = width - gap - p - 1
subpattern = pattern[1:]
subconstraints = constraints[-subwidth:]
for s in solve2(subwidth, subpattern, subconstraints):
yield (gap, s[0]+1) + s[1:]
def invariants(width, pattern, constraints=None):
"compute invariants"
invs = []
for sol in solve2(width, pattern, constraints):
exp = list(expand_solution(sol, width, pattern))
if len(invs) == 0:
invs = exp
else:
for i, e in enumerate(exp):
if invs[i] != e:
invs[i] = -1
return invs
def visual(constraints):
"returns a visual representation of constraints"
return "".join({
1:'\N{FULL BLOCK}\N{LEFT SEVEN EIGHTHS BLOCK}',
0:'__',
-1:'??'
}[x] for x in constraints)
class Board:
"""Board
A board is actually a list of constraints.
A cell with 1 or 0 is fixed. A cell with -1
doesn't have a known value yet.
"""
def __init__(self, patterns):
self.col_patterns = patterns[0]
self.row_patterns = patterns[1]
self.width = len(patterns[0])
self.height = len(patterns[1])
self.rows = [None] * self.height
for i in range(self.height):
self.rows[i] = [-1] * self.width
# print("rows:")
# for y in range(self.height):
# n, c = invariants(self.width, self.row_patterns[y])
# print(n, self.row_patterns[y], visual(c))
# print("cols:")
# for x in range(self.height):
# n, c = invariants(self.width, self.col_patterns[x])
# print(n, self.col_patterns[x], visual(c))
# print(self.row(0))
def col(self, i):
"""a column"""
return [self.rows[x][i] for x in range(self.height)]
def row(self, i):
"""a row"""
return self.rows[i]
def replace_row(self, i, row):
self.rows[i] = row
def replace_col(self, i , col):
for y in range(self.height):
self.rows[y][i] = col[y]
def compute_invariants(self):
while True:
changed = False
row_sols = [0] * self.height
col_sols = [0] * self.width
# rows
for y in range(self.height):
invs = None; count = 0
for sol in solve2(self.width, self.row_patterns[y], self.row(y)):
count += 1
exp = list(expand_solution(sol, self.width, self.row_patterns[y]))
if invs == None:
invs = exp
for i, e in enumerate(exp):
if invs[i] != e:
invs[i] = -1
if invs != None and self.row(y) != invs:
self.replace_row(y, invs)
changed = True
row_sols[y] = count
# columns
for x in range(self.width):
invs = None; count = 0
for sol in solve2(self.height, self.col_patterns[x], self.col(x)):
count += 1
exp = list(expand_solution(sol, self.height, self.col_patterns[x]))
if invs == None:
invs = exp
for i, e in enumerate(exp):
if invs[i] != e:
invs[i] = -1
if invs != None and self.col(x) != invs:
self.replace_col(x, invs)
changed = True
col_sols[x] = count
if not changed:
break
return row_sols, col_sols
def solve(self, solved=lambda x: None, depth=0):
row_sols, col_sols = self.compute_invariants()
# if depth < 2:
# print("depth:", depth)
# print(self)
# print("row_sols:", row_sols)
# print("col_sols:", col_sols)
if min(row_sols) == 0 or min(col_sols) == 0:
return False
if max(row_sols) == 1:
print("solved")
solved(self)
return True
min_row, y = min((a,b) for b,a in enumerate(row_sols) if a > 1)
min_col, x = min((a,b) for b,a in enumerate(col_sols) if a > 1)
if min_row < min_col:
for sol in solve2(self.width, self.row_patterns[y], self.row(y)):
b = self.copy()
b.replace_row(y, expand_solution(sol, self.width, self.row_patterns[y]))
if b.solve(solved, depth + 1):
return True
else:
for sol in solve2(self.height, self.col_patterns[x], self.col(x)):
b = self.copy()
b.replace_col(x, expand_solution(sol, self.height, self.col_patterns[x]))
if b.solve(solved, depth + 1):
return True
def copy(self):
newboard = Board((self.col_patterns, self.row_patterns))
for i in range(self.height):
newboard.rows[i] = self.rows[i][:]
return newboard
def __str__(self):
s = ""
for y in range(self.height):
s += visual(self.rows[y])
s += "\n"
return s
if __name__ == "__main__":
def draw(solution, width, pattern):
"draws a solution"
for s, p in zip(solution, pattern):
print('__' * s, end="")
print('\N{FULL BLOCK}\N{LEFT SEVEN EIGHTHS BLOCK}' * p, end="")
print('__' * (width - sum(solution) - sum(pattern)))
# width = int(sys.argv[1])
# pattern = tuple(int(x) for x in sys.argv[2].split())
# constraints = [-1] * width
# try:
# for i, c in enumerate(sys.argv[3]):
# constraints[i] = {'1':1, '0':0, '?':-1}[c]
# except:
# constraints = [-1] * width
# for solution in solve1(width, pattern):
# e = expand_solution(solution, width, pattern)
# if matches(e, constraints):
# draw(solution, width, pattern)
def parse(rows):
"parses '1 1, 1 2 3' into [[1, 1], [1, 2, 3]]"
rows = rows.split(",")
rows = [[int(y) for y in x.strip().split()] for x in rows]
return rows
def parse_constraints(s, width):
constraints = [-1] * width
for i, c in enumerate(s):
constraints[i] = {'1':1, '0':0, '?':-1, '.':0, '\N{LEFT SEVEN EIGHTHS BLOCK}': 1}[c]
return constraints
# width = 15
# pattern = (1, 1, 1, 2)
# constraints = parse_constraints('????????010?', width)
# for sol in solve2(width, pattern, constraints):
# draw(sol, width, pattern)
# b = Board((
# parse("1 1 1, 1 1 1, 1 1 1, 1 1 1, 1 1 1, 1 1 1"),
# parse("1 1 1, 1 1 1, 1 1 1, 1 1 1, 1 1 1, 1 1 1")
# ))
# b.solve(print)
# b = Board((
# parse("""1 1 1 1 1, 1 1 1, 1 1 1 1, 1 2, 1 1 1 1, 1 1 1, 1 1 1,
# 3 1, 1 1, 1 2 6 1, 2 1, 2 3 1, 1 1, 1 1 3 1, 2 1 1"""),
# parse("""1 2, 1 1 2, 2 1 1 1 1, 3 1, 1 1 1 1, 1 2 1, 1, 1 1 1 2,
# 2 2 1 1, 1 1 1 1 1, 1 2 2, 2 2, 1 1 1 1 1, 1 1 1 1, 1 1""")
# ))
# b.solve(print)
# b = Board((
# parse("""2 4, 1 1 3 1, 6, 3 3, 3 4, 1 3 2, 3 4 1, 3 5 1 1, 12,
# 5 3 3, 6 4, 2 3, 1 2 2, 3 4, 3 5"""),
# parse("""1 6, 2 2 4 2, 3 5 2, 11, 1 6 1, 4 1 5, 5 3 3, 10 2, 1 7 1 1, 5 1 1 1,
# 1 3 1, 3, 3, 1 1, 3"""),
# ))
# b.solve()
c = Board((
parse("""1 5 2, 1 1 2, 1 1 2 1 2, 2 2, 2 1 1 1, 1 1 1, 1 1, 1 1 1, 2 3 1 1,
1 2 3 1 1, 1 3 1 1, 2 1 1 1, 1 1 1 2 1, 1 1 1 2 1, 2 1"""),
parse("""1 2 1 1, 1 1 4, 2 1, 1 1 1 1 2, 1 3 1 1, 1 2, 1 1 1 1 1 1,
1 1 1 1 1 2, 1 1 2, 1 2 1 1, 3 1 4, 1 4 1, 3, 3 1 1, 1 2 1""")
))
c.solve(print)
# c = Board(([[2, 1], [2, 1, 1], [1, 1, 1, 1], [2, 1], [1, 1, 1], [1, 1, 1, 1], [3], [3, 2, 1], [1, 1, 3, 1],
# [1, 1, 1, 1], [1, 2, 1, 1, 1], [1, 1, 1, 1], [1, 2, 1], [1], [1, 4, 1]],
# [[1, 2, 1], [1, 3, 1], [1, 1, 3], [1, 1, 1], [1, 1], [1, 1], [1, 2, 1, 1, 1], [2, 3, 3, 1, 1],
# [1, 1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1, 1, 1], [1], [1, 2], [1, 1], [1, 1, 1]]))
# c.solve(print)
if len(sys.argv) > 2:
b = Board((parse(sys.argv[1]), parse(sys.argv[2])))
b.solve(print)