nonograms/main.py

323 lines
10 KiB
Python
Raw Normal View History

2016-11-20 22:17:41 -05:00
#!/usr/bin/env python3
2016-11-22 05:14:32 -05:00
"This module does something"
2016-11-20 22:17:41 -05:00
from itertools import combinations_with_replacement
2016-11-22 05:14:32 -05:00
import sys
2016-11-20 22:17:41 -05:00
2016-11-20 23:54:36 -05:00
def solve1(width, pattern):
2016-11-20 22:17:41 -05:00
"""
This yields a tuple for each possible layout of
pattern inside the row. The tuple elements are the
2016-11-20 23:54:36 -05:00
gaps before each block in pattern.
The tuple doesn't include the last gap, since that's
2016-11-22 05:14:32 -05:00
just: width - sum(sol) - sum(pattern)
2016-11-20 22:17:41 -05:00
"""
spaces = width - (sum(pattern) + len(pattern) - 1)
for sol in combinations_with_replacement(range(spaces + 1), len(pattern)):
2016-11-22 05:14:32 -05:00
sol = sol[0:1] + tuple((sol[i] - sol[i-1] + 1) for i in range(1, len(sol)))
2016-11-20 22:17:41 -05:00
yield sol
def expand_solution(solution, width, pattern):
"""
2016-11-22 05:14:32 -05:00
expands a solution to a tuple of 1 (ON) and 0 (OFF)
2016-11-20 22:17:41 -05:00
"""
r = []
2016-11-22 05:14:32 -05:00
for s, p in zip(solution, pattern):
2016-11-20 22:17:41 -05:00
r.extend([0] * s)
r.extend([1] * p)
r.extend([0] * (width - sum(solution) - sum(pattern)))
return tuple(r)
def matches(expanded_solution, constraints):
"""
2016-11-20 23:54:36 -05:00
solution is a tuple of spaces, the output of solve1
2016-11-20 22:17:41 -05:00
constraints is a tuple of values from 1, 0 and -1, that
mean:
0 -> OFF
1 -> ON
-1 -> not constrained
"""
2016-11-22 05:14:32 -05:00
for s, c in zip(expanded_solution, constraints):
2016-11-20 22:17:41 -05:00
if c == -1:
continue
if c != s:
return False
return True
2016-11-20 23:54:36 -05:00
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
2016-11-22 05:14:32 -05:00
in consideration to be faster than solve1 + matches
2016-11-20 23:54:36 -05:00
"""
2016-11-22 05:14:32 -05:00
2016-11-20 23:54:36 -05:00
if len(pattern) == 0:
return tuple()
if constraints is None:
constraints = [-1] * width
2016-11-22 05:14:32 -05:00
2016-11-20 23:54:36 -05:00
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):
2016-11-22 06:49:57 -05:00
# constraint width
cw = (gap + p + 1) if len(pattern) > 1 else width
e = expand_solution((gap,), cw, (p,))
if not matches(e, constraints[:cw]):
2016-11-20 23:54:36 -05:00
continue
if len(pattern) == 1:
yield (gap,)
continue
subwidth = width - gap - p - 1
subpattern = pattern[1:]
2016-11-22 05:14:32 -05:00
subconstraints = constraints[-subwidth:]
2016-11-20 23:54:36 -05:00
for s in solve2(subwidth, subpattern, subconstraints):
2016-11-22 05:14:32 -05:00
yield (gap, s[0]+1) + s[1:]
2016-11-20 23:54:36 -05:00
2016-11-22 05:14:32 -05:00
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
2016-11-22 07:44:11 -05:00
return invs
2016-11-22 05:14:32 -05:00
def visual(constraints):
"returns a visual representation of constraints"
2016-11-22 07:44:11 -05:00
return "".join({
1:'\N{FULL BLOCK}\N{LEFT SEVEN EIGHTHS BLOCK}',
0:'__',
-1:'??'
}[x] for x in constraints)
2016-11-22 05:14:32 -05:00
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
2016-11-22 07:44:11 -05:00
# 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))
2016-11-22 05:14:32 -05:00
2016-11-22 07:44:11 -05:00
# 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))
2016-11-22 05:14:32 -05:00
2016-11-22 07:44:11 -05:00
# print(self.row(0))
2016-11-22 05:14:32 -05:00
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]
2016-11-22 07:44:11 -05:00
def replace_row(self, i, row):
self.rows[i] = row
def replace_col(self, i , col):
2016-11-22 05:14:32 -05:00
for y in range(self.height):
2016-11-22 07:44:11 -05:00
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:
2016-11-22 05:14:32 -05:00
for sol in solve2(self.width, self.row_patterns[y], self.row(y)):
2016-11-22 07:44:11 -05:00
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
2016-11-22 05:14:32 -05:00
2016-11-22 07:44:11 -05:00
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
2016-11-20 23:54:36 -05:00
2016-11-20 22:17:41 -05:00
if __name__ == "__main__":
def draw(solution, width, pattern):
2016-11-22 05:14:32 -05:00
"draws a solution"
for s, p in zip(solution, pattern):
2016-11-22 07:44:11 -05:00
print('__' * s, end="")
print('\N{FULL BLOCK}\N{LEFT SEVEN EIGHTHS BLOCK}' * p, end="")
2016-11-22 10:03:30 -05:00
print('__' * (width - sum(solution) - sum(pattern)))
2016-11-20 22:17:41 -05:00
2016-11-22 07:44:11 -05:00
# 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
2016-11-20 22:17:41 -05:00
2016-11-20 23:54:36 -05:00
# for solution in solve1(width, pattern):
# e = expand_solution(solution, width, pattern)
# if matches(e, constraints):
# draw(solution, width, pattern)
2016-11-22 05:14:32 -05:00
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
2016-11-22 07:44:11 -05:00
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)
2016-11-22 05:14:32 -05:00
2016-11-22 07:44:11 -05:00
# 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)
2016-11-22 05:14:32 -05:00
2016-11-22 07:44:11 -05:00
# 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"""),
2016-11-22 05:14:32 -05:00
# ))
2016-11-22 07:44:11 -05:00
# 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)
2016-11-21 13:56:18 -05:00
2016-11-22 10:03:30 -05:00
# 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)