|
- import collections
- import functools
- import itertools
- import math
- import re
- import sys
-
-
- def render(grid):
- lines = []
- for y in range(size):
- groups = [tiles[grid[y, x]].splitlines()[1:-1] for x in range(size)]
- for lns in zip(*groups):
- lines.append(''.join(ln[1:-1] for ln in lns))
- return '\n'.join(lines)
-
-
- def get_borders(content):
- a, *_, b = content.splitlines()
- c, *_, d = [''.join(ln) for ln in zip(*content.splitlines())]
- return [a, b, c, d, a[::-1], b[::-1], c[::-1], d[::-1]]
-
-
- def transpose(string):
- return '\n'.join(''.join(ln) for ln in zip(*string.splitlines()))
-
-
- def flipV(string):
- return '\n'.join(ln for ln in string.splitlines()[::-1])
-
-
- def flipH(string):
- return '\n'.join(ln[::-1] for ln in string.splitlines())
-
-
- def rot90(string):
- return flipH(transpose(string))
-
-
- def noop(string):
- return string
-
-
- def variants(string):
- alts = [noop, flipH], [noop, flipV], [noop, transpose]
- for ops in itertools.product(*alts):
- yield functools.reduce(lambda s, f: f(s), ops, string)
-
-
- def UD(A, B):
- return A.splitlines()[-1] == B.splitlines()[0]
-
-
- def LR(A, B):
- return UD(rot90(A), rot90(B))
-
-
- def search_in(source, target):
- source_lns = source.splitlines()
- target_lns = target.splitlines()
- h, w = len(target_lns), len(target_lns[0])
- out = []
- for y in range(len(source_lns)):
- for x in range(len(source_lns[0])):
- window = '\n'.join(ln[x:x + w] for ln in source_lns[y:y + h])
- if re.fullmatch(target, window):
- out.append((x, y))
- return out
-
-
- def reconcile(A, B, condition):
- tiles[A], tiles[B] = next(
- (X, Y)
- for X, Y in itertools.product(variants(tiles[A]), variants(tiles[B]))
- if condition(X, Y)
- )
-
-
- text = sys.stdin.read().strip()
- tiles = {}
- borders = {}
- for line in text.split('\n\n'):
- title, content = line.split(':\n')
- tid = int(title.split()[1])
- tiles[tid] = content
- borders[tid] = get_borders(content)
- size = int(len(tiles) ** 0.5)
-
- adj = collections.defaultdict(set)
- for (aid, A), (bid, B) in itertools.combinations(borders.items(), 2):
- if set(A) & set(B):
- adj[aid].add(bid)
- adj[bid].add(aid)
- corners = [v for v, ks in adj.items() if len(ks) == 2]
- print(math.prod(corners))
-
-
- A = min(corners)
- B, C = adj.pop(A)
-
- grid = {(y, x): None for x in range(size) for y in range(size)}
- grid[0, 0] = A
- grid[0, 1] = B
- grid[1, 0] = C
- reconcile(A, B, condition=LR)
- reconcile(A, C, condition=UD)
-
- seen = {A, B, C}
- for z in range(2, 2 * size - 1):
- # determine new pieces from intersection between 2 neighbors
- for y, x in {(y, z - y) for y in range(1, z)}.intersection(grid):
- U, L = (y - 1, x), (y, x - 1)
- grid[y, x], = adj[grid[U]] & adj[grid[L]] - seen
- reconcile(grid[L], grid[y, x], condition=LR)
- seen.add(grid[y, x])
-
- # determine new piece from only one alternative left
- for y, x in {(0, z), (z, 0)}.intersection(grid):
- opts = {(y - 1, x): UD, (y, x - 1): LR}
- P, fn = next(p for p in opts.items() if p[0] in grid)
- grid[y, x], = adj[grid[P]] - seen
- reconcile(grid[P], grid[y, x], condition=fn)
- seen.add(grid[y, x])
-
- monster = '''
- ..................#.
- #....##....##....###
- .#..#..#..#..#..#...
- '''[1:-1]
- src = render(grid)
- found = max([search_in(pic, monster) for pic in variants(src)], key=len)
- print(src.count('#') - len(found) * monster.count('#'))
|