2023/day11/common.py
2023-12-11 11:15:05 +01:00

140 lines
4.6 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
import logging
from itertools import combinations
# from pprint import pformat
from typing import NamedTuple
class Coordinate(NamedTuple):
x: int
y: int
c: str
def is_galaxy(self):
return self.c == "#"
def distance(
self, other: Coordinate, universe: Universe | None = None, factor: int = 1
) -> int:
current = self
movements = 0
while current != other:
if current.x < other.x:
movements += 1
current = Coordinate(current.x + 1, current.y, current.c)
elif current.x > other.x:
movements += 1
current = Coordinate(current.x - 1, current.y, current.c)
if universe is not None:
if current.x in universe.empty_cols:
movements += -1 + factor
if current.y < other.y:
movements += 1
current = Coordinate(current.x, current.y + 1, current.c)
elif current.y > other.y:
movements += 1
current = Coordinate(current.x, current.y - 1, current.c)
if universe is not None:
if current.y in universe.empty_lines:
movements += -1 + factor
logging.debug(f"distance between {self} and {other} = {movements}")
return movements
def pairs_of_stars(universe: Universe) -> list[tuple[Coordinate, Coordinate]]:
return list(combinations(universe.stars, 2))
@dataclass(init=False)
class Universe:
_data: list[list[Coordinate]]
stars: list[Coordinate]
empty_lines: list[int] | None
empty_cols: list[int] | None
def __init__(self, data: list[list[Coordinate]], part: int = 1):
self._data = data
if part == 1:
self._expand_universe()
self.stars = []
stars = [filter(lambda c: c.is_galaxy(), line) for line in self._data]
for line in stars:
self.stars.extend(line)
if part == 2:
self.empty_cols = []
self.empty_lines = []
for x in range(len(self._data[0])):
if all(
map(lambda c: not c.is_galaxy(), [line[x] for line in self._data])
):
self.empty_cols.append(x)
for line in self._data:
if all(map(lambda c: not c.is_galaxy(), line)):
self.empty_lines.append(line[0].y)
def __getitem__(self, key: int) -> list[Coordinate]:
return self._data[key]
def __len__(self):
return len(self._data)
def __iter__(self):
return iter(self._data)
def _expand_universe(self) -> None:
# universe expansion on x
# first find columns with no galaxy
empty_cols: list[int] = []
for x in range(len(self._data[0])):
if all(map(lambda c: not c.is_galaxy(), [line[x] for line in self._data])):
empty_cols.append(x)
for y, line in enumerate(self._data):
x_offset = 0
new_line = []
for col in line:
if x_offset > 0:
new_col = Coordinate(col.x + x_offset, col.y, col.c)
new_line.append(new_col)
else:
new_line.append(col)
if col.x in empty_cols:
x_offset += 1
new_col = Coordinate(col.x + x_offset, col.y, col.c)
new_line.append(new_col)
self._data[y] = new_line
# universe expansion on y
new_universe = []
y_offset = 0
for y, line in enumerate(self._data):
if y_offset > 0:
new_line = [Coordinate(o.x, o.y + y_offset, o.c) for o in line]
new_universe.append(new_line)
else:
new_universe.append(line)
if all(map(lambda c: not c.is_galaxy(), line)):
y_offset += 1
new_line = [Coordinate(o.x, o.y + y_offset, o.c) for o in line]
new_universe.append(new_line)
self._data = new_universe
# logging.debug(pformat(self._data))
def get_map(self):
return self._data
def parse(input_file: str, part: int = 1) -> Universe:
data = []
y = 0
with open(input_file, "r", encoding="utf-8") as input_fd:
while line := input_fd.readline():
data.append(
[Coordinate(x, y, c) for x, c in enumerate(line.rstrip("\n").rstrip())]
)
y += 1
# logging.debug(pformat(data))
return Universe(data, part)