mirror of
https://github.com/Noettore/AdventOfCode.git
synced 2025-10-14 19:26:39 +02:00
307 lines
7.3 KiB
Python
307 lines
7.3 KiB
Python
"""AOC 2020 Day 20"""
|
|
|
|
import pathlib
|
|
import time
|
|
import collections
|
|
import operator
|
|
import itertools
|
|
|
|
TEST_INPUT = """Tile 2311:
|
|
..##.#..#.
|
|
##..#.....
|
|
#...##..#.
|
|
####.#...#
|
|
##.##.###.
|
|
##...#.###
|
|
.#.#.#..##
|
|
..#....#..
|
|
###...#.#.
|
|
..###..###
|
|
|
|
Tile 1951:
|
|
#.##...##.
|
|
#.####...#
|
|
.....#..##
|
|
#...######
|
|
.##.#....#
|
|
.###.#####
|
|
###.##.##.
|
|
.###....#.
|
|
..#.#..#.#
|
|
#...##.#..
|
|
|
|
Tile 1171:
|
|
####...##.
|
|
#..##.#..#
|
|
##.#..#.#.
|
|
.###.####.
|
|
..###.####
|
|
.##....##.
|
|
.#...####.
|
|
#.##.####.
|
|
####..#...
|
|
.....##...
|
|
|
|
Tile 1427:
|
|
###.##.#..
|
|
.#..#.##..
|
|
.#.##.#..#
|
|
#.#.#.##.#
|
|
....#...##
|
|
...##..##.
|
|
...#.#####
|
|
.#.####.#.
|
|
..#..###.#
|
|
..##.#..#.
|
|
|
|
Tile 1489:
|
|
##.#.#....
|
|
..##...#..
|
|
.##..##...
|
|
..#...#...
|
|
#####...#.
|
|
#..#.#.#.#
|
|
...#.#.#..
|
|
##.#...##.
|
|
..##.##.##
|
|
###.##.#..
|
|
|
|
Tile 2473:
|
|
#....####.
|
|
#..#.##...
|
|
#.##..#...
|
|
######.#.#
|
|
.#...#.#.#
|
|
.#########
|
|
.###.#..#.
|
|
########.#
|
|
##...##.#.
|
|
..###.#.#.
|
|
|
|
Tile 2971:
|
|
..#.#....#
|
|
#...###...
|
|
#.#.###...
|
|
##.##..#..
|
|
.#####..##
|
|
.#..####.#
|
|
#..#.#..#.
|
|
..####.###
|
|
..#.#.###.
|
|
...#.#.#.#
|
|
|
|
Tile 2729:
|
|
...#.#.#.#
|
|
####.#....
|
|
..#.#.....
|
|
....#..#.#
|
|
.##..##.#.
|
|
.#.####...
|
|
####.#.#..
|
|
##.####...
|
|
##..#.##..
|
|
#.##...##.
|
|
|
|
Tile 3079:
|
|
#.#.#####.
|
|
.#..######
|
|
..#.......
|
|
######....
|
|
####.#..#.
|
|
.#...#.##.
|
|
#.#####.##
|
|
..#.###...
|
|
..#.......
|
|
..#.###..."""
|
|
|
|
MONSTER_PATTERN = (
|
|
' # ',
|
|
'# ## ## ###',
|
|
' # # # # # # '
|
|
)
|
|
|
|
def read_input(input_path: str) -> str:
|
|
"""take input file path and return a str with the file's content"""
|
|
with open(input_path, 'r') as input_file:
|
|
input_data = input_file.read().strip()
|
|
return input_data
|
|
|
|
def extract(input_data: str) -> dict:
|
|
"""take input data and return the appropriate data structure"""
|
|
tiles = dict()
|
|
for tile in input_data.split('\n\n'):
|
|
parts = tile.split('\n')
|
|
tile_id = int(parts[0][5:-1])
|
|
tiles[tile_id] = parts[1:]
|
|
return tiles
|
|
|
|
def edge(matrix: list, side: str) -> str:
|
|
"""return a side of a matrix"""
|
|
if side == 'n':
|
|
return matrix[0]
|
|
if side == 's':
|
|
return matrix[-1]
|
|
if side == 'e':
|
|
return ''.join(map(operator.itemgetter(-1), matrix))
|
|
return ''.join(map(operator.itemgetter(0), matrix))
|
|
|
|
def get_tiles_corners(tiles: dict) -> dict:
|
|
"""return the corners of a set of tiles"""
|
|
matching_sides = collections.defaultdict(str)
|
|
corners = {}
|
|
|
|
for id_tile1, id_tile2 in itertools.combinations(tiles, 2):
|
|
tile1, tile2 = tiles[id_tile1], tiles[id_tile2]
|
|
|
|
for side_a in 'nsew':
|
|
for side_b in 'nsew':
|
|
edge_a, edge_b = edge(tile1, side_a), edge(tile2, side_b)
|
|
|
|
if edge_a == edge_b or edge_a == edge_b[::-1]:
|
|
matching_sides[id_tile1] += side_a
|
|
matching_sides[id_tile2] += side_b
|
|
|
|
for tid, sides in matching_sides.items():
|
|
if len(sides) == 2:
|
|
corners[tid] = sides
|
|
|
|
assert len(corners) == 4
|
|
return corners
|
|
|
|
def rotate90(matrix: list) -> tuple:
|
|
"""return the matrix rotated by 90"""
|
|
return tuple(''.join(column)[::-1] for column in zip(*matrix))
|
|
|
|
def possible_orientations(matrix: list) -> list:
|
|
"""return all possible orientations of a given matrix"""
|
|
orientations = [matrix]
|
|
for _ in range(3):
|
|
matrix = rotate90(matrix)
|
|
orientations.append(matrix)
|
|
return orientations
|
|
|
|
def possible_arrangements(matrix: list) -> list:
|
|
"""return all possible arrangements of a given matrix"""
|
|
arrangements = possible_orientations(matrix)
|
|
arrangements.extend(possible_orientations(matrix[::-1]))
|
|
return arrangements
|
|
|
|
def strip_edges(matrix: list) -> list:
|
|
"""return a matrix without it's edges"""
|
|
return [row[1:-1] for row in matrix[1:-1]]
|
|
|
|
def matching_tile(tile: list, tiles: dict, side_a: str, side_b: str) -> list:
|
|
"""return the adjacent tile of a given one"""
|
|
edge_a = edge(tile, side_a)
|
|
|
|
for other_id, other_tile in tiles.items():
|
|
if tile is other_tile:
|
|
continue
|
|
|
|
for other_arr in possible_arrangements(other_tile):
|
|
if edge_a == edge(other_arr, side_b):
|
|
tiles.pop(other_id)
|
|
return other_arr
|
|
|
|
def matching_row(prev: list, tiles: dict, tiles_per_row: int) -> list:
|
|
"""return matching tiles in the row of a given one"""
|
|
matching_tiles = [prev]
|
|
tile = prev
|
|
for _ in range(tiles_per_row - 1):
|
|
tile = matching_tile(tile, tiles, 'e', 'w')
|
|
matching_tiles.append(tile)
|
|
return matching_tiles
|
|
|
|
def build_image(top_left_tile: list, tiles: dict, image_dimension: int) -> list:
|
|
"""build an image of a set of tiles"""
|
|
first = top_left_tile
|
|
image = []
|
|
|
|
while 1:
|
|
image_row = matching_row(first, tiles, image_dimension)
|
|
image_row = map(strip_edges, image_row)
|
|
image.extend(map(''.join, zip(*image_row)))
|
|
|
|
if not tiles:
|
|
break
|
|
|
|
first = matching_tile(first, tiles, 's', 'n')
|
|
|
|
return image
|
|
|
|
def count_pattern(image: list, pattern: tuple) -> int:
|
|
"""return the number of times a given pattern appears in an image"""
|
|
pattern_h, pattern_w = len(pattern), len(pattern[0])
|
|
image_sz = len(image)
|
|
deltas = []
|
|
|
|
for row_index, row in enumerate(pattern):
|
|
for cell_index, cell in enumerate(row):
|
|
if cell == '#':
|
|
deltas.append((row_index, cell_index))
|
|
|
|
for img in possible_arrangements(image):
|
|
appearances = 0
|
|
for row_index in range(image_sz - pattern_h):
|
|
for cell_index in range(image_sz - pattern_w):
|
|
if all(img[row_index + dr][cell_index + dc] == '#' for dr, dc in deltas):
|
|
appearances += 1
|
|
|
|
if appearances != 0:
|
|
return appearances
|
|
|
|
def part1(entries: dict) -> int:
|
|
"""part1 solver"""
|
|
corners = get_tiles_corners(entries)
|
|
corners_prod = 1
|
|
for tile_id in corners:
|
|
corners_prod *= tile_id
|
|
return corners_prod
|
|
|
|
def part2(entries: dict) -> int:
|
|
"""part2 solver"""
|
|
corners = get_tiles_corners(entries)
|
|
top_left_id, matching_sides = corners.popitem()
|
|
top_left = entries[top_left_id]
|
|
|
|
if matching_sides in ('ne', 'en'):
|
|
top_left = rotate90(top_left)
|
|
elif matching_sides in ('nw', 'wn'):
|
|
top_left = rotate90(rotate90(top_left))
|
|
elif matching_sides in ('sw', 'ws'):
|
|
top_left = rotate90(rotate90(rotate90(top_left)))
|
|
|
|
image_dimension = int(len(entries) ** 0.5)
|
|
entries.pop(top_left_id)
|
|
|
|
image = build_image(top_left, entries, image_dimension)
|
|
monster_cells = sum(row.count('#') for row in MONSTER_PATTERN)
|
|
water_cells = sum(row.count('#') for row in image)
|
|
n_monsters = count_pattern(image, MONSTER_PATTERN)
|
|
|
|
return water_cells - n_monsters * monster_cells
|
|
|
|
def test_input_day_20():
|
|
"""pytest testing function"""
|
|
entries = extract(TEST_INPUT)
|
|
assert part1(entries) == 20899048083289
|
|
assert part2(entries) == 273
|
|
|
|
def test_bench_day_20(benchmark):
|
|
"""pytest-benchmark function"""
|
|
benchmark(main)
|
|
|
|
def main():
|
|
"""main function"""
|
|
input_path = str(pathlib.Path(__file__).resolve().parent.parent) + "/inputs/" + str(pathlib.Path(__file__).stem)
|
|
start_time = time.time()
|
|
input_data = read_input(input_path)
|
|
entries = extract(input_data)
|
|
print("Part 1: %d" % part1(entries))
|
|
print("Part 2: %d" % part2(entries))
|
|
end_time = time.time()
|
|
print("Execution time: %f" % (end_time-start_time))
|
|
|
|
if __name__ == "__main__":
|
|
main()
|