|
|
@@ -1,4 +1,3 @@ |
|
|
|
import collections |
|
|
|
import functools |
|
|
|
import itertools |
|
|
|
import math |
|
|
@@ -8,125 +7,89 @@ 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): |
|
|
|
for row in grid: |
|
|
|
for lns in zip(*[cell.splitlines()[1:-1] for cell in row]): |
|
|
|
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 |
|
|
|
class Ops: |
|
|
|
noop = lambda txt: txt # noqa |
|
|
|
flipV = lambda txt: '\n'.join(ln for ln in txt.splitlines()[::-1]) # noqa |
|
|
|
flipH = lambda txt: '\n'.join(ln[::-1] for ln in txt.splitlines()) # noqa |
|
|
|
transpose = lambda txt: '\n'.join(map(''.join, zip(*txt.splitlines()))) # noqa |
|
|
|
|
|
|
|
|
|
|
|
def variants(string): |
|
|
|
alts = [noop, flipH], [noop, flipV], [noop, transpose] |
|
|
|
for ops in itertools.product(*alts): |
|
|
|
alts = [Ops.flipH, Ops.flipV, Ops.transpose] |
|
|
|
for ops in itertools.product(*[[Ops.noop, fn] for fn in 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 |
|
|
|
yield (x, y) |
|
|
|
|
|
|
|
|
|
|
|
def find(old, new): |
|
|
|
delta = new - old |
|
|
|
base = fixed[old] |
|
|
|
|
|
|
|
if delta == -1: goal = [ln[0] for ln in base.splitlines()] # noqa |
|
|
|
elif delta == 1: goal = [ln[-1] for ln in base.splitlines()] # noqa |
|
|
|
elif delta == 1j: goal = base.splitlines()[-1] # noqa |
|
|
|
elif delta == -1j: goal = base.splitlines()[0] # noqa |
|
|
|
|
|
|
|
for tid, tile in tiles.items(): |
|
|
|
for var in variants(tile): |
|
|
|
|
|
|
|
if delta == -1: found = [ln[-1] for ln in var.splitlines()] # noqa |
|
|
|
elif delta == 1: found = [ln[0] for ln in var.splitlines()] # noqa |
|
|
|
elif delta == 1j: found = var.splitlines()[0] # noqa |
|
|
|
elif delta == -1j: found = var.splitlines()[-1] # noqa |
|
|
|
|
|
|
|
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) |
|
|
|
) |
|
|
|
if goal == found: |
|
|
|
tiles.pop(tid) |
|
|
|
grid[new] = tid |
|
|
|
fixed[new] = var |
|
|
|
yield tid |
|
|
|
return |
|
|
|
|
|
|
|
|
|
|
|
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]) |
|
|
|
start = min(tiles, key=tiles.get) |
|
|
|
fixed = {0: tiles.pop(start)} |
|
|
|
grid = {0: start} |
|
|
|
while tiles: |
|
|
|
grid.update({ |
|
|
|
new: good |
|
|
|
for old in list(grid) |
|
|
|
for new in {old - 1, old + 1, old + 1j, old - 1j} - set(fixed) |
|
|
|
for good in find(old, new) |
|
|
|
}) |
|
|
|
|
|
|
|
xs = xmin, *_, xmax = sorted({int(p.real) for p in fixed}) |
|
|
|
ys = ymin, *_, ymax = sorted({int(p.imag) for p in fixed}) |
|
|
|
print(math.prod(grid[x + 1j * y] for x in [xmin, xmax] for y in [ymin, ymax])) |
|
|
|
|
|
|
|
monster = ''' |
|
|
|
..................#. |
|
|
|
#....##....##....### |
|
|
|
.#..#..#..#..#..#... |
|
|
|
'''[1:-1] |
|
|
|
src = render(grid) |
|
|
|
found = max([search_in(pic, monster) for pic in variants(src)], key=len) |
|
|
|
src = render([[fixed[complex(x, y)] for x in xs] for y in ys]) |
|
|
|
found = max([list(search_in(pic, monster)) for pic in variants(src)], key=len) |
|
|
|
print(src.count('#') - len(found) * monster.count('#')) |