advent-of-code/2020/day11_seating.py
2020-12-12 18:48:47 +01:00

177 lines
5.9 KiB
Python

import copy
import functools
import itertools
from collections import Counter
from pprint import pprint
from typing import List, Tuple, Iterable, Dict, Optional
import networkx as nx
def main(filename: str, expected_part_1: int = None, expected_part_2: int = None):
print(f"\n+ Running on {filename}")
with open(filename) as f:
seat_map = f.read()
seat_map_part_1 = SeatMap(seat_map)
seat_map_part_1.evolve_until_stable()
counter_part_1 = seat_map_part_1.total_number_of_occupied()
seat_map_part_2 = SeatMapPart2(seat_map)
seat_map_part_2.evolve_until_stable()
counter_part_2 = seat_map_part_2.total_number_of_occupied()
print(f"1. Found {counter_part_1}")
if expected_part_1:
assert expected_part_1 == counter_part_1
print(f"2. Found {counter_part_2}")
if expected_part_2:
assert expected_part_2 == counter_part_2
Coordinates = Tuple[int, int]
class SeatMap:
OCCUPIED = "#"
FREE = "L"
FLOOR = "."
EMPTY_THRESHOLD = 4
def __init__(self, seat_map: str):
self._map = dict() # type: Dict[Coordinates, str]
rows = seat_map.split("\n")
for row_id, row in enumerate(rows):
for col_id, cell in enumerate(row):
self._map[(row_id, col_id)] = cell
self._map_height = len(rows)
self._map_width = len(rows[0])
def __str__(self):
final = []
for row_id in range(0, self._map_height):
row = []
for col_id in range(0, self._map_width):
row.append(self._square_at((row_id, col_id)))
final.append("".join(row))
return "\n".join(final) + "\n"
def _square_at(self, coordinates: Coordinates) -> str:
return self._map[coordinates]
def total_number_of_occupied(self) -> int:
counter = Counter("".join(self._map.values()))
return counter[self.OCCUPIED]
def evolve_until_stable(self):
old_map = self._map
next_stage_map = self._evolve()
while next_stage_map != old_map:
old_map = next_stage_map
next_stage_map = self._evolve()
def _evolve(self):
next_stage_map = dict()
for coord, square in self._map.items():
next_stage_map[coord] = self._square_evolve(square, coord)
self._map = next_stage_map
return self._map
def _square_evolve(self, square: str, coordinates: Coordinates) -> str:
if square == self.FLOOR:
return square
occupied_adjacent = self._number_of_occupied_adjacent(coordinates)
if square == self.FREE and occupied_adjacent == 0:
return self.OCCUPIED
if square == self.OCCUPIED and occupied_adjacent >= self.EMPTY_THRESHOLD:
return self.FREE
return square
def _number_of_occupied_adjacent(self, coordinates: Coordinates) -> int:
count_occupied = 0
for seat in self._visible_seats(coordinates):
if seat == self.OCCUPIED:
count_occupied += 1
return count_occupied
def _visible_seats(self, coordinates: Coordinates) -> List[str]:
adjacent_cells = [
self._find_top_left(coordinates),
self._find_top(coordinates),
self._find_top_right(coordinates),
self._find_right(coordinates),
self._find_bottom_right(coordinates),
self._find_bottom(coordinates),
self._find_bottom_left(coordinates),
self._find_left(coordinates),
]
return list(map(self._square_at, filter(None, adjacent_cells)))
@functools.lru_cache(None)
def _find_top_left(self, coordinates: Coordinates) -> Optional[Coordinates]:
return self._find_with_delta(coordinates, (-1, -1))
@functools.lru_cache(None)
def _find_with_delta(
self, coordinates: Coordinates, delta: Tuple[int, int]
) -> Optional[Coordinates]:
other_coord = (coordinates[0] + delta[0], coordinates[1] + delta[1])
try:
self._square_at(other_coord)
except KeyError:
return None
return other_coord
@functools.lru_cache(None)
def _find_top(self, coordinates: Coordinates) -> Optional[Coordinates]:
return self._find_with_delta(coordinates, (-1, 0))
@functools.lru_cache(None)
def _find_top_right(self, coordinates: Coordinates) -> Optional[Coordinates]:
return self._find_with_delta(coordinates, (-1, 1))
@functools.lru_cache(None)
def _find_right(self, coordinates: Coordinates) -> Optional[Coordinates]:
return self._find_with_delta(coordinates, (0, 1))
@functools.lru_cache(None)
def _find_bottom_right(self, coordinates: Coordinates) -> Optional[Coordinates]:
return self._find_with_delta(coordinates, (1, 1))
@functools.lru_cache(None)
def _find_bottom(self, coordinates: Coordinates) -> Optional[Coordinates]:
return self._find_with_delta(coordinates, (1, 0))
@functools.lru_cache(None)
def _find_bottom_left(self, coordinates: Coordinates) -> Optional[Coordinates]:
return self._find_with_delta(coordinates, (1, -1))
@functools.lru_cache(None)
def _find_left(self, coordinates: Coordinates) -> Optional[Coordinates]:
return self._find_with_delta(coordinates, (0, -1))
class SeatMapPart2(SeatMap):
EMPTY_THRESHOLD = 5
@functools.lru_cache(None)
def _find_with_delta(
self, coordinates: Coordinates, delta: Tuple[int, int]
) -> Optional[Coordinates]:
other_coord = (coordinates[0] + delta[0], coordinates[1] + delta[1])
try:
other_square = self._square_at(other_coord)
while other_square == self.FLOOR:
other_coord = (other_coord[0] + delta[0], other_coord[1] + delta[1])
other_square = self._square_at(other_coord)
except KeyError:
return None
return other_coord
if __name__ == "__main__":
main("inputs/day11-test1", 37, 26)
main("inputs/day11", 2324, 2068)