diff --git a/day11/common.py b/day11/common.py new file mode 100644 index 0000000..1c5ce40 --- /dev/null +++ b/day11/common.py @@ -0,0 +1,139 @@ +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) diff --git a/day11/example_input.txt b/day11/example_input.txt new file mode 100644 index 0000000..a0bda53 --- /dev/null +++ b/day11/example_input.txt @@ -0,0 +1,10 @@ +...#...... +.......#.. +#......... +.......... +......#... +.#........ +.........# +.......... +.......#.. +#...#..... \ No newline at end of file diff --git a/day11/part1.py b/day11/part1.py new file mode 100755 index 0000000..8e26ec7 --- /dev/null +++ b/day11/part1.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3.11 +from common import * + + +def part1(lpairs_of_stars: list[tuple[Coordinate, Coordinate]]) -> int: + logging.debug(f"pairs={lpairs_of_stars}") + distances = list(map(lambda gs: gs[0].distance(gs[1]), lpairs_of_stars)) + logging.debug(f"distances={distances}") + return sum(distances) + + +if __name__ == "__main__": + # logging.basicConfig(level=logging.DEBUG) + print(part1(pairs_of_stars(parse("input.txt")))) diff --git a/day11/part2.py b/day11/part2.py new file mode 100755 index 0000000..1e28f43 --- /dev/null +++ b/day11/part2.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3.11 + +from common import * + + +def part2(universe: Universe, factor: int) -> int: + pairs = pairs_of_stars(universe) + logging.debug(f"pairs={pairs}") + distances = list(map(lambda gs: gs[0].distance(gs[1], universe, factor), pairs)) + logging.debug(f"distances={distances}") + return sum(distances) + + +if __name__ == "__main__": + print(part2(parse("input.txt", part=2), 1000000)) diff --git a/day11/test.py b/day11/test.py new file mode 100755 index 0000000..3240d0f --- /dev/null +++ b/day11/test.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3.11 +import logging +from unittest import TestCase, main + +from common import Coordinate, parse, pairs_of_stars + +from part1 import part1 +from part2 import part2 + + +class Day11Test(TestCase): + def test_parse(self): + universe_map = [ + [Coordinate(x, 0, ".") for x in range(4)] + + [Coordinate(4, 0, "#")] + + [Coordinate(x, 0, ".") for x in range(5, 13)], + [Coordinate(x, 1, ".") for x in range(9)] + + [Coordinate(9, 1, "#")] + + [Coordinate(x, 1, ".") for x in range(10, 13)], + [Coordinate(0, 2, "#")] + [Coordinate(x, 2, ".") for x in range(1, 13)], + [Coordinate(x, 3, ".") for x in range(13)], + [Coordinate(x, 4, ".") for x in range(13)], + [Coordinate(x, 5, ".") for x in range(8)] + + [Coordinate(8, 5, "#")] + + [Coordinate(x, 5, ".") for x in range(9, 13)], + [Coordinate(0, 6, "."), Coordinate(1, 6, "#")] + + [Coordinate(x, 6, ".") for x in range(2, 13)], + [Coordinate(x, 7, ".") for x in range(12)] + [Coordinate(12, 7, "#")], + [Coordinate(x, 8, ".") for x in range(13)], + [Coordinate(x, 9, ".") for x in range(13)], + [Coordinate(x, 10, ".") for x in range(9)] + + [Coordinate(9, 10, "#")] + + [Coordinate(x, 10, ".") for x in range(10, 13)], + [Coordinate(0, 11, "#")] + + [Coordinate(x, 11, ".") for x in range(1, 5)] + + [Coordinate(5, 11, "#")] + + [Coordinate(x, 11, ".") for x in range(6, 13)], + ] + stars = [ + Coordinate(4, 0, "#"), + Coordinate(9, 1, "#"), + Coordinate(0, 2, "#"), + Coordinate(8, 5, "#"), + Coordinate(1, 6, "#"), + Coordinate(12, 7, "#"), + Coordinate(9, 10, "#"), + Coordinate(0, 11, "#"), + Coordinate(5, 11, "#"), + ] + universe = parse("example_input.txt") + self.assertEqual(universe_map, universe.get_map()) + self.assertEqual(stars, universe.stars) + + def test_part1(self): + self.assertEqual(374, part1(pairs_of_stars(parse("example_input.txt")))) + + def test_part2(self): + self.assertEqual(1030, part2(parse("example_input.txt", part=2), factor=10)) + self.assertEqual(8410, part2(parse("example_input.txt", part=2), factor=100)) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + main(verbosity=2)