另一种 Nonogram Solver

#gossip

翻出很久以前写的一个 nonogram 求解器,它不显式建模黑白格,而是通过前缀计数间接建模。

例如一行是"3 2 4",通常建成这一行有3黑格,至少1白格,2黑格,1白格,4黑格。 而这种求解方法把它建模成,每一个格子的数字,表示这一行到目前位置走过了多少个黑格。 又因为行、列都不超过99,通过除余100来将行、列走过的黑格数建到一起(v = 100 * col + row)。 这样"3 2 4"在行上的数字可能就是"1 2 3 3 4 5 5 5 5 5 6 7 8 9 9", 计数值的停留表示了黑块的结束。 为了保证两个黑块间有一个白格,我们要求边界前缀值出现不止一次,而块内部的前缀值只能出现一次。

相比常规解法,这种解法更间接,更不直观,更慢,但很好玩。

from z3 import *  # noqa: F403
from colorama import Fore, Back, Style

COLS = '''
4
1 4
1 5 2
1 4 4
7 6
5 6
5
5
7
8
9
8
8
7
4
'''

ROWS = '''
3
1 1
3 2 4
6 6
6 7
5 7
4 8
1 8
9
9
7
4
3
4
3
'''

def _to_sum(str):
  str_to_int_lst = lambda lst: list(map(lambda x: int(x), lst))
  rows_str = map(lambda y: y.split(' '), 
             filter(lambda x: len(x) > 0, str.split('\n')))
  rows = map(lambda x: str_to_int_lst(x), rows_str)
  return list(rows)

cols = _to_sum(COLS)
rows = _to_sum(ROWS)
SIZE = len(cols)

solver = Solver()
input = [Int(f'a_{x+1}_{y+1}') for x in range(SIZE) for y in range(SIZE)]
elem = lambda row, col: input[row * SIZE + col]
for i in range(SIZE):
  for j in range(SIZE):
    solver.add(elem(i, j) >= 0)
for i in range(SIZE):
  for j in range(SIZE - 1):
    solver.add(Or([elem(i, j+1) % 100 == elem(i, j) % 100, elem(i, j+1) % 100 == elem(i, j) % 100 + 1]))
    solver.add(Or([elem(j+1, i) / 100 == elem(j, i) / 100, elem(j+1, i) / 100 == elem(j, i) / 100 + 1]))

# cols
for i in range(SIZE):
  col = []
  for j in range(SIZE):
    col.append(input[j * SIZE + i])
  solver.add(Or([col[0] / 100 == 0, col[0] / 100 == 1]))
  total = 0
  totals = []
  for s in cols[i]:
    total += s
    totals.append(total)
  totals.pop()
  for j in range(1, total):
    cnt = Sum([If(j == x / 100, 1, 0) for x in col])
    if j in totals:
      solver.add(cnt > 1)
    else:
      solver.add(cnt == 1)
  solver.add(elem(SIZE - 1, i) / 100 == total)

# rows
for i in range(SIZE):
  row = input[i * SIZE:i * SIZE + SIZE]
  solver.add(Or([row[0] % 100 == 0, row[0] % 100 == 1]))
  total = 0
  totals = []
  for s in rows[i]:
    total += s
    totals.append(total)
  totals.pop()
  for j in range(1, total):
    cnt = Sum([If(j == x % 100, 1, 0) for x in row])
    if j in totals:
      solver.add(cnt > 1)
    else:
      solver.add(cnt == 1)
  solver.add(elem(i, SIZE - 1) % 100 == total)

# implies
# first block
solver.add(elem(0, 0) % 100 == elem(0, 0) / 100)
for i in range(1, SIZE):
  # first col
  solver.add(Implies(elem(i, 0) % 100 == 0, elem(i, 0) / 100 == elem(i-1, 0) / 100))
  solver.add(Implies(elem(i, 0) % 100 == 1, elem(i, 0) / 100 == elem(i-1, 0) / 100 + 1))
  # first row
  solver.add(Implies(elem(0, i) / 100 == 0, elem(0, i) % 100 == elem(0, i-1) % 100))
  solver.add(Implies(elem(0, i) / 100 == 1, elem(0, i) % 100 == elem(0, i-1) % 100 + 1))
# other blocks
for col in range(1, SIZE):
  for row in range(1, SIZE):
    solver.add(Implies(elem(row, col) / 100 == elem(row-1, col) / 100 + 1, elem(row, col) % 100 == elem(row, col-1) % 100 + 1))
    solver.add(Implies(elem(row, col) / 100 == elem(row-1, col) / 100, elem(row, col) % 100 == elem(row, col-1) % 100))

print(solver.check())

## output
my_j = -2
def my_print(s, end='\n'):
  global my_j
  my_j += 1
  l = len(s)
  r = l // 10 + 1
  for i in range(r):
    if (i % 2 == 0) ^ ((my_j // 5) % 2 == 0):
      print(Back.WHITE + Fore.BLACK + s[i*10:10+i*10], end='')
    else:
      print(Back.BLACK + Fore.WHITE + s[i*10:10+i*10], end='')
  print(Style.RESET_ALL, end=end)

m = solver.model()
result = []
for i in range(SIZE):
  old_v = 0
  line = []
  line_block = []
  for j in range(SIZE):
    v = m.evaluate(m[input[i * SIZE + j]]).as_long()
    line.append(v)
    line_block.append('■' if (v % 100) > (old_v % 100) else ' ')
    old_v = v
    # print(x, end='')
  # print('')
  print(line)
  result.append(line_block)
print('#.', end='')
my_print('.'.join(map(lambda x: str(x%10), range(1, SIZE + 1))), end='.\n')
k = 0
for i in result:
  k += 1
  print(f'{k%10}.', end='')
  my_print('.'.join(i), end='.\n')