from __future__ import annotations from array import array from collections.abc import Callable, Iterable from dataclasses import dataclass from itertools import accumulate, combinations from .aoc import Aoc2, AocSameParser @dataclass class Position: x: int y: int def __add__(self, other: Position) -> Position: return Position(self.x + other.x, self.y + other.y) def __sub__(self, other: Position) -> Position: return Position(self.x - other.x, self.y - other.y) def __or__(self, other: Position) -> int: return abs(self.x - other.x) + abs(self.y - other.y) def __rmul__(self, other: int) -> Position: return Position(other * self.x, other * self.y) def __getitem__(self, pi: int) -> int: if pi == 0: return self.x return self.y def __repr__(self) -> str: return f"[{self.x},{self.y}]" class Matrix: def __init__(self, inpt: str) -> None: lines = inpt.splitlines() self.height = len(lines) self.width = len(lines[0]) self.array = array("u") self.array_rot = array("u") for line in lines: self.array.fromlist(list(line)) for line in zip(*lines): self.array_rot.fromlist(list(line)) def __getitem__(self, pos: Position | tuple[int, int]) -> str: return self.array[self.width * pos[1] + pos[0]] def __setitem__(self, pos: Position | tuple[int, int], value: str) -> None: self.array[self.width * pos[1] + pos[0]] = value def get_row(self, row_no: int) -> array[str]: return self.array[self.width * row_no : self.width * (row_no + 1)] def iter_rows(self) -> Iterable[array[str]]: return (self.get_row(y) for y in range(self.height)) def get_col(self, col_no: int) -> array[str]: return self.array_rot[self.height * col_no : self.width * (col_no + 1)] # return array("u", [self[col_no, y] for y in range(self.height)]) def iter_cols(self) -> Iterable[array[str]]: return (self.get_col(x) for x in range(self.width)) def __str__(self) -> str: return "\n".join((x.tounicode() for x in self.iter_rows())) def finditer(self, element: str) -> Iterable[Position]: return ( Position(x, y) for y, line in enumerate(self.iter_rows()) for x, char in enumerate(line) if char == element ) def filter_row_ids(self, predicate: Callable[[array[str]], bool]) -> Iterable[int]: return (y for y, row in enumerate(self.iter_rows()) if predicate(row)) def filter_col_ids(self, predicate: Callable[[array[str]], bool]) -> Iterable[int]: return (x for x, col in enumerate(self.iter_cols()) if predicate(col)) @dataclass class Map: map: Matrix galaxies: Iterable[tuple[Position, Position]] @classmethod def from_input(cls, inpt: str) -> Map: matrix = Matrix(inpt) missing_xs = array( "I", accumulate([1 if col.count("#") == 0 else 0 for col in matrix.iter_cols()]), ) missing_ys = array( "I", accumulate([1 if row.count("#") == 0 else 0 for row in matrix.iter_rows()]), ) offset_galaxies = ( ( Position( missing_xs[pos.x], missing_ys[pos.y], ), pos, ) for pos in matrix.finditer("#") ) return cls(matrix, offset_galaxies) class Day11(AocSameParser[Map], Aoc2[Map, Map]): def parseinput(self, inpt: str) -> Map: return Map.from_input(inpt) def part1(self, inpt: Map) -> int: return sum( g1 | g2 for g1, g2 in combinations(((o + g) for o, g in inpt.galaxies), 2) ) def part2(self, inpt: Map) -> int: return sum( g1 | g2 for g1, g2 in combinations(((999_999 * o + g) for o, g in inpt.galaxies), 2) ) def main() -> None: day11 = Day11(2023, 11) day11.run() if __name__ == "__main__": main()