from collections import defaultdict from lib import get_data, ints from math import isqrt data = get_data(__file__) def top(rows): return tuple(rows[0]) def bottom(rows): return tuple(rows[-1]) def left(rows): return tuple(tuple(row[0] for row in rows)) def right(rows): return tuple(tuple(row[-1] for row in rows)) def fliph(rows): return tuple(tuple(row[::-1] for row in rows)) def flipv(rows): return tuple(rows[::-1]) def rot90(rows): return tuple(tuple(row) for row in zip(*rows[::-1])) def rot180(rows): return rot90(rot90(rows)) def rot270(rows): return rot90(rot90(rot90(rows))) TOP = 0 RIGHT = 1 BOTTOM = 2 LEFT = 3 def all(rows) -> list: return [ rows, rot90(rows), rot180(rows), rot270(rows), fliph(rows), flipv(rows), rot90(fliph(rows)), rot90(flipv(rows)), # rot180(fliph(rows)), # rot180(flipv(rows)), # rot270(fliph(rows)), # rot270(flipv(rows)), ] tiles = [] for p in data.strip().split("\n\n"): lines = p.splitlines() (id,) = ints(lines[0]) rowst = tuple(map(tuple, lines[1:])) tiles.append((id, all(rowst))) rights = defaultdict(list) bottoms = defaultdict(list) for id, variants in tiles: for variant in variants: rights[left(variant)].append((id, variant)) bottoms[top(variant)].append((id, variant)) num_tiles = len(tiles) rows = isqrt(len(tiles)) def dfs(id_set, id_list, tiles_used): if len(tiles_used) == num_tiles: return id_list, tiles_used i = len(tiles_used) id_variants = None if i < rows: # first row id_variants = rights[right(tiles_used[i - 1])] elif i % rows == 0: # first tile in row id_variants = bottoms[bottom(tiles_used[i - rows])] else: id_variants = set(bottoms[bottom(tiles_used[i - rows])]) id_variants &= set(rights[right(tiles_used[i - 1])]) id_variants = list(id_variants) assert id_variants is not None for id, variant in id_variants: if id in id_set: continue id_set.add(id) id_list.append(id) tiles_used.append(variant) r = dfs(id_set, id_list, tiles_used) if r is not False: return r tiles_used.pop() id_set.remove(id) id_list.pop() return False, None tiles_used = None for id, tile_variants in tiles: for tile in tile_variants: r, tiles_used = dfs(set([id]), [id], [tile]) if type(r) is list: print(r[0] * r[-1] * r[rows - 1] * r[-rows]) break if tiles_used is not None: break else: print("no result") exit(0) tiles_used = [[tile[x][1:-1] for x in range(1, len(tile) - 1)] for tile in tiles_used] merged_tiles = [] for i in range(0, rows * rows, rows): for row in range(len(tiles_used[0])): merged_row = [] for j in range(rows): merged_row.extend(tiles_used[i + j][row]) merged_tiles.append(merged_row) seemonster = """ # # ## ## ### # # # # # # """ offsets: list[tuple[int, int]] = [] for row, line in enumerate(seemonster.splitlines()): for col, c in enumerate(line): if c == "#": offsets.append((row, col)) total_hashes = sum([row.count("#") for row in merged_tiles]) seemonster_hashes = len(offsets) max_seemonster_count = 0 for field in all(merged_tiles): seemonster_count = 0 for ri in range(len(field) - 2): for ci in range(len(field[0]) - 19): for ro, co in offsets: if field[ri + ro][ci + co] != "#": break else: seemonster_count += 1 max_seemonster_count = max(max_seemonster_count, seemonster_count) print(total_hashes - max_seemonster_count * seemonster_hashes)