From ce315b8778164327d96322c5e572e0aa4fad6265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sun, 8 Dec 2024 13:06:41 +0000 Subject: [PATCH] Refactor code for API (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikael CAPELLE Co-authored-by: MikaĆ«l Capelle Reviewed-on: https://gitea.typename.fr/mikael.capelle/advent-of-code/pulls/3 --- poetry.lock | 16 +- pyproject.toml | 1 + src/holt59/aoc/2015/day1.py | 16 +- src/holt59/aoc/2015/day10.py | 29 +- src/holt59/aoc/2015/day11.py | 16 +- src/holt59/aoc/2015/day12.py | 16 +- src/holt59/aoc/2015/day13.py | 41 +- src/holt59/aoc/2015/day14.py | 85 ++- src/holt59/aoc/2015/day15.py | 67 +- src/holt59/aoc/2015/day16.py | 56 +- src/holt59/aoc/2015/day17.py | 26 +- src/holt59/aoc/2015/day18.py | 96 +-- src/holt59/aoc/2015/day19.py | 106 +-- src/holt59/aoc/2015/day2.py | 30 +- src/holt59/aoc/2015/day20.py | 15 +- src/holt59/aoc/2015/day21.py | 52 +- src/holt59/aoc/2015/day22.py | 63 +- src/holt59/aoc/2015/day3.py | 13 +- src/holt59/aoc/2015/day4.py | 26 +- src/holt59/aoc/2015/day5.py | 16 +- src/holt59/aoc/2015/day6.py | 49 +- src/holt59/aoc/2015/day7.py | 105 ++- src/holt59/aoc/2015/day8.py | 57 +- src/holt59/aoc/2015/day9.py | 37 +- src/holt59/aoc/2021/day1.py | 23 +- src/holt59/aoc/2021/day10.py | 12 +- src/holt59/aoc/2021/day11.py | 12 +- src/holt59/aoc/2021/day12.py | 12 +- src/holt59/aoc/2021/day13.py | 12 +- src/holt59/aoc/2021/day14.py | 12 +- src/holt59/aoc/2021/day15.py | 12 +- src/holt59/aoc/2021/day16.py | 12 +- src/holt59/aoc/2021/day17.py | 12 +- src/holt59/aoc/2021/day18.py | 12 +- src/holt59/aoc/2021/day19.py | 12 +- src/holt59/aoc/2021/day2.py | 57 +- src/holt59/aoc/2021/day20.py | 12 +- src/holt59/aoc/2021/day21.py | 12 +- src/holt59/aoc/2021/day22.py | 12 +- src/holt59/aoc/2021/day23.py | 12 +- src/holt59/aoc/2021/day24.py | 12 +- src/holt59/aoc/2021/day25.py | 12 +- src/holt59/aoc/2021/day3.py | 38 +- src/holt59/aoc/2021/day4.py | 73 +- src/holt59/aoc/2021/day5.py | 74 +- src/holt59/aoc/2021/day6.py | 32 +- src/holt59/aoc/2021/day7.py | 33 +- src/holt59/aoc/2021/day8.py | 104 +-- src/holt59/aoc/2021/day9.py | 39 +- src/holt59/aoc/2022/day1.py | 15 +- src/holt59/aoc/2022/day10.py | 71 +- src/holt59/aoc/2022/day11.py | 47 +- src/holt59/aoc/2022/day12.py | 121 +-- src/holt59/aoc/2022/day13.py | 27 +- src/holt59/aoc/2022/day14.py | 131 ++-- src/holt59/aoc/2022/day15.py | 147 ++-- src/holt59/aoc/2022/day16.py | 63 +- src/holt59/aoc/2022/day17.py | 52 +- src/holt59/aoc/2022/day18.py | 78 +- src/holt59/aoc/2022/day19.py | 62 +- src/holt59/aoc/2022/day2.py | 34 +- src/holt59/aoc/2022/day20.py | 15 +- src/holt59/aoc/2022/day21.py | 47 +- src/holt59/aoc/2022/day22.py | 420 +++++----- src/holt59/aoc/2022/day23.py | 59 +- src/holt59/aoc/2022/day24.py | 175 +++-- src/holt59/aoc/2022/day25.py | 41 +- src/holt59/aoc/2022/day3.py | 39 +- src/holt59/aoc/2022/day4.py | 17 +- src/holt59/aoc/2022/day5.py | 58 +- src/holt59/aoc/2022/day6.py | 13 +- src/holt59/aoc/2022/day7.py | 137 ++-- src/holt59/aoc/2022/day8.py | 81 +- src/holt59/aoc/2022/day9.py | 26 +- src/holt59/aoc/2023/day1.py | 50 +- src/holt59/aoc/2023/day10.py | 174 ++--- src/holt59/aoc/2023/day11.py | 63 +- src/holt59/aoc/2023/day12.py | 50 +- src/holt59/aoc/2023/day13.py | 32 +- src/holt59/aoc/2023/day14.py | 58 +- src/holt59/aoc/2023/day15.py | 42 +- src/holt59/aoc/2023/day16.py | 54 +- src/holt59/aoc/2023/day17.py | 348 ++++----- src/holt59/aoc/2023/day18.py | 40 +- src/holt59/aoc/2023/day19.py | 210 +++-- src/holt59/aoc/2023/day2.py | 68 +- src/holt59/aoc/2023/day20.py | 267 +++---- src/holt59/aoc/2023/day21.py | 255 ++++--- src/holt59/aoc/2023/day22.py | 170 ++--- src/holt59/aoc/2023/day23.py | 116 ++- src/holt59/aoc/2023/day24.py | 109 +-- src/holt59/aoc/2023/day25.py | 32 +- src/holt59/aoc/2023/day3.py | 74 +- src/holt59/aoc/2023/day4.py | 53 +- src/holt59/aoc/2023/day5.py | 132 ++-- src/holt59/aoc/2023/day6.py | 40 +- src/holt59/aoc/2023/day7.py | 29 +- src/holt59/aoc/2023/day8.py | 43 +- src/holt59/aoc/2023/day9.py | 49 +- src/holt59/aoc/2024/day1.py | 21 +- src/holt59/aoc/2024/day10.py | 11 +- src/holt59/aoc/2024/day11.py | 11 +- src/holt59/aoc/2024/day12.py | 11 +- src/holt59/aoc/2024/day13.py | 11 +- src/holt59/aoc/2024/day14.py | 11 +- src/holt59/aoc/2024/day15.py | 11 +- src/holt59/aoc/2024/day16.py | 11 +- src/holt59/aoc/2024/day17.py | 11 +- src/holt59/aoc/2024/day18.py | 11 +- src/holt59/aoc/2024/day19.py | 11 +- src/holt59/aoc/2024/day2.py | 33 +- src/holt59/aoc/2024/day20.py | 11 +- src/holt59/aoc/2024/day21.py | 11 +- src/holt59/aoc/2024/day22.py | 11 +- src/holt59/aoc/2024/day23.py | 11 +- src/holt59/aoc/2024/day24.py | 11 +- src/holt59/aoc/2024/day25.py | 11 +- src/holt59/aoc/2024/day3.py | 54 +- src/holt59/aoc/2024/day4.py | 58 +- src/holt59/aoc/2024/day5.py | 41 +- src/holt59/aoc/2024/day6.py | 62 +- src/holt59/aoc/2024/day7.py | 52 +- src/holt59/aoc/2024/day8.py | 78 +- src/holt59/aoc/2024/day9.py | 11 +- src/holt59/aoc/__main__.py | 161 +++- src/holt59/aoc/base.py | 34 + src/holt59/aoc/inputs/holt59/2024/day7.txt | 850 +++++++++++++++++++++ src/holt59/aoc/inputs/holt59/2024/day8.txt | 50 ++ src/holt59/aoc/inputs/tests/2024/day7.txt | 9 + src/holt59/aoc/inputs/tests/2024/day8.txt | 12 + 130 files changed, 4599 insertions(+), 3336 deletions(-) create mode 100644 src/holt59/aoc/base.py diff --git a/poetry.lock b/poetry.lock index 3897c84..ac71c61 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1245,6 +1245,20 @@ files = [ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] +[[package]] +name = "types-networkx" +version = "3.4.2.20241115" +description = "Typing stubs for networkx" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-networkx-3.4.2.20241115.tar.gz", hash = "sha256:d669b650cf6c6c9ec879a825449eb04a5c10742f3109177e1683f57ee49e0f59"}, + {file = "types_networkx-3.4.2.20241115-py3-none-any.whl", hash = "sha256:f0c382924d6614e06bf0b1ca0b837b8f33faa58982bc086ea762efaf39aa98dd"}, +] + +[package.dependencies] +numpy = ">=1.20" + [[package]] name = "typing-extensions" version = "4.12.2" @@ -1281,4 +1295,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "b643261f91a781d77735e05f6d2ac1002867600c2df6393a9d1a15f5e1189109" +content-hash = "c91bc307ff4a5b3e8cd1976ebea211c9749fe09d563dd80861f70ce26826cda9" diff --git a/pyproject.toml b/pyproject.toml index e991388..2fd2d8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ ruff = "^0.8.1" poethepoet = "^0.31.1" ipykernel = "^6.29.5" networkx-stubs = "^0.0.1" +types-networkx = "^3.4.2.20241115" [tool.poetry.scripts] holt59-aoc = "holt59.aoc.__main__:main" diff --git a/src/holt59/aoc/2015/day1.py b/src/holt59/aoc/2015/day1.py index 8ddf9bc..0c3e64b 100644 --- a/src/holt59/aoc/2015/day1.py +++ b/src/holt59/aoc/2015/day1.py @@ -1,10 +1,12 @@ -import sys +from typing import Any, Iterator -line = sys.stdin.read().strip() - -floor = 0 -floors = [(floor := floor + (1 if c == "(" else -1)) for c in line] +from ..base import BaseSolver -print(f"answer 1 is {floors[-1]}") -print(f"answer 2 is {floors.index(-1)}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + floor = 0 + floors = [(floor := floor + (1 if c == "(" else -1)) for c in input] + + yield floors[-1] + yield floors.index(-1) diff --git a/src/holt59/aoc/2015/day10.py b/src/holt59/aoc/2015/day10.py index 55cdef2..f7569d4 100644 --- a/src/holt59/aoc/2015/day10.py +++ b/src/holt59/aoc/2015/day10.py @@ -1,7 +1,7 @@ import itertools -import sys +from typing import Any, Iterator -line = sys.stdin.read().strip() +from ..base import BaseSolver # see http://www.se16.info/js/lands2.htm for the explanation of 'atoms' (or elements) # @@ -9,7 +9,7 @@ line = sys.stdin.read().strip() # CodeGolf answer https://codegolf.stackexchange.com/a/8479/42148 # fmt: off -atoms = [ +ATOMS: list[tuple[str, tuple[int, ...]]] = [ ("22", (0, )), # 0 ("13112221133211322112211213322112", (71, 90, 0, 19, 2, )), # 1 ("312211322212221121123222112", (1, )), # 2 @@ -105,7 +105,7 @@ atoms = [ ] # fmt: on -starters = [ +STARTERS = [ "1", "11", "21", @@ -122,27 +122,26 @@ def look_and_say_length(s: str, n: int) -> int: if n == 0: return len(s) - if s in starters: + if s in STARTERS: return look_and_say_length( "".join(f"{len(list(g))}{k}" for k, g in itertools.groupby(s)), n - 1 ) - counts = {i: 0 for i in range(len(atoms))} - idx = next(i for i, (a, _) in enumerate(atoms) if s == a) + counts = {i: 0 for i in range(len(ATOMS))} + idx = next(i for i, (a, _) in enumerate(ATOMS) if s == a) counts[idx] = 1 for _ in range(n): - c2 = {i: 0 for i in range(len(atoms))} + c2 = {i: 0 for i in range(len(ATOMS))} for i in counts: - for j in atoms[i][1]: + for j in ATOMS[i][1]: c2[j] += counts[i] counts = c2 - return sum(counts[i] * len(a[0]) for i, a in enumerate(atoms)) + return sum(counts[i] * len(a[0]) for i, a in enumerate(ATOMS)) -answer_1 = look_and_say_length(line, 40) -print(f"answer 1 is {answer_1}") - -answer_2 = look_and_say_length(line, 50) -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any] | None: + yield look_and_say_length(input, 40) + yield look_and_say_length(input, 50) diff --git a/src/holt59/aoc/2015/day11.py b/src/holt59/aoc/2015/day11.py index 7d9ff1f..259ccf9 100644 --- a/src/holt59/aoc/2015/day11.py +++ b/src/holt59/aoc/2015/day11.py @@ -1,5 +1,7 @@ import itertools -import sys +from typing import Any, Iterator + +from ..base import BaseSolver def is_valid(p: str) -> bool: @@ -40,10 +42,8 @@ def find_next_password(p: str) -> str: return p -line = sys.stdin.read().strip() - -answer_1 = find_next_password(line) -print(f"answer 1 is {answer_1}") - -answer_2 = find_next_password(increment(answer_1)) -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + answer_1 = find_next_password(input) + yield answer_1 + yield find_next_password(increment(answer_1)) diff --git a/src/holt59/aoc/2015/day12.py b/src/holt59/aoc/2015/day12.py index bf1667d..9faabb6 100644 --- a/src/holt59/aoc/2015/day12.py +++ b/src/holt59/aoc/2015/day12.py @@ -1,6 +1,7 @@ import json -import sys -from typing import TypeAlias +from typing import Any, Iterator, TypeAlias + +from ..base import BaseSolver JsonObject: TypeAlias = dict[str, "JsonObject"] | list["JsonObject"] | int | str @@ -18,10 +19,9 @@ def json_sum(value: JsonObject, ignore: str | None = None) -> int: return 0 -data: JsonObject = json.load(sys.stdin) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + data: JsonObject = json.loads(input) -answer_1 = json_sum(data) -print(f"answer 1 is {answer_1}") - -answer_2 = json_sum(data, "red") -print(f"answer 2 is {answer_2}") + yield json_sum(data) + yield json_sum(data, "red") diff --git a/src/holt59/aoc/2015/day13.py b/src/holt59/aoc/2015/day13.py index ef0d7a7..3113abd 100644 --- a/src/holt59/aoc/2015/day13.py +++ b/src/holt59/aoc/2015/day13.py @@ -1,10 +1,11 @@ import itertools -import sys from collections import defaultdict -from typing import Literal, cast +from typing import Any, Iterator, Literal, cast import parse # type: ignore +from ..base import BaseSolver + def max_change_in_happiness(happiness: dict[str, dict[str, int]]) -> int: guests = list(happiness) @@ -17,25 +18,23 @@ def max_change_in_happiness(happiness: dict[str, dict[str, int]]) -> int: ) -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -happiness: dict[str, dict[str, int]] = defaultdict(dict) -for line in lines: - u1, gain_or_loose, hap, u2 = cast( - tuple[str, Literal["gain", "lose"], int, str], - parse.parse( # type: ignore - "{} would {} {:d} happiness units by sitting next to {}.", line - ), - ) - happiness[u1][u2] = hap if gain_or_loose == "gain" else -hap + happiness: dict[str, dict[str, int]] = defaultdict(dict) + for line in lines: + u1, gain_or_loose, hap, u2 = cast( + tuple[str, Literal["gain", "lose"], int, str], + parse.parse( # type: ignore + "{} would {} {:d} happiness units by sitting next to {}.", line + ), + ) + happiness[u1][u2] = hap if gain_or_loose == "gain" else -hap + yield max_change_in_happiness(happiness) + for guest in list(happiness): + happiness["me"][guest] = 0 + happiness[guest]["me"] = 0 -answer_1 = max_change_in_happiness(happiness) -print(f"answer 1 is {answer_1}") - -for guest in list(happiness): - happiness["me"][guest] = 0 - happiness[guest]["me"] = 0 - -answer_2 = max_change_in_happiness(happiness) -print(f"answer 2 is {answer_2}") + yield max_change_in_happiness(happiness) diff --git a/src/holt59/aoc/2015/day14.py b/src/holt59/aoc/2015/day14.py index 7f2b1d2..a17e521 100644 --- a/src/holt59/aoc/2015/day14.py +++ b/src/holt59/aoc/2015/day14.py @@ -1,9 +1,10 @@ -import sys from dataclasses import dataclass -from typing import Literal, cast +from typing import Any, Iterator, Literal, cast import parse # type: ignore +from ..base import BaseSolver + @dataclass(frozen=True) class Reindeer: @@ -13,50 +14,50 @@ class Reindeer: rest_time: int -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -reindeers: list[Reindeer] = [] -for line in lines: - reindeer, speed, speed_time, rest_time = cast( - tuple[str, int, int, int], - parse.parse( # type: ignore - "{} can fly {:d} km/s for {:d} seconds, " - "but then must rest for {:d} seconds.", - line, - ), - ) - reindeers.append( - Reindeer(name=reindeer, speed=speed, fly_time=speed_time, rest_time=rest_time) - ) + reindeers: list[Reindeer] = [] + for line in lines: + reindeer, speed, speed_time, rest_time = cast( + tuple[str, int, int, int], + parse.parse( # type: ignore + "{} can fly {:d} km/s for {:d} seconds, " + "but then must rest for {:d} seconds.", + line, + ), + ) + reindeers.append( + Reindeer( + name=reindeer, speed=speed, fly_time=speed_time, rest_time=rest_time + ) + ) -target = 1000 if len(reindeers) <= 2 else 2503 + target = 1000 if len(reindeers) <= 2 else 2503 -states: dict[Reindeer, tuple[Literal["resting", "flying"], int]] = { - reindeer: ("resting", 0) for reindeer in reindeers -} -distances: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers} -points: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers} + states: dict[Reindeer, tuple[Literal["resting", "flying"], int]] = { + reindeer: ("resting", 0) for reindeer in reindeers + } + distances: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers} + points: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers} -for time in range(target): - for reindeer in reindeers: - if states[reindeer][0] == "flying": - distances[reindeer] += reindeer.speed + for time in self.progress.wrap(range(target)): + for reindeer in reindeers: + if states[reindeer][0] == "flying": + distances[reindeer] += reindeer.speed - top_distance = max(distances.values()) - for reindeer in reindeers: - if distances[reindeer] == top_distance: - points[reindeer] += 1 + top_distance = max(distances.values()) + for reindeer in reindeers: + if distances[reindeer] == top_distance: + points[reindeer] += 1 - for reindeer in reindeers: - if states[reindeer][1] == time: - if states[reindeer][0] == "resting": - states[reindeer] = ("flying", time + reindeer.fly_time) - else: - states[reindeer] = ("resting", time + reindeer.rest_time) + for reindeer in reindeers: + if states[reindeer][1] == time: + if states[reindeer][0] == "resting": + states[reindeer] = ("flying", time + reindeer.fly_time) + else: + states[reindeer] = ("resting", time + reindeer.rest_time) - -answer_1 = max(distances.values()) -print(f"answer 1 is {answer_1}") - -answer_2 = max(points.values()) - 1 -print(f"answer 2 is {answer_2}") + yield max(distances.values()) + yield max(points.values()) - 1 diff --git a/src/holt59/aoc/2015/day15.py b/src/holt59/aoc/2015/day15.py index 953dabf..9a894b0 100644 --- a/src/holt59/aoc/2015/day15.py +++ b/src/holt59/aoc/2015/day15.py @@ -1,9 +1,10 @@ import math -import sys -from typing import Sequence, cast +from typing import Any, Iterator, Sequence, cast import parse # type: ignore +from ..base import BaseSolver + def score(ingredients: list[list[int]], teaspoons: Sequence[int]) -> int: return math.prod( @@ -18,40 +19,38 @@ def score(ingredients: list[list[int]], teaspoons: Sequence[int]) -> int: ) -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -ingredients: list[list[int]] = [] -for line in lines: - _, *scores = cast( - tuple[str, int, int, int, int, int], - parse.parse( # type: ignore - "{}: capacity {:d}, durability {:d}, flavor {:d}, " - "texture {:d}, calories {:d}", - line, - ), - ) - ingredients.append(scores) - -total_teaspoons = 100 -calories: list[int] = [] -scores: list[int] = [] - -for a in range(total_teaspoons + 1): - for b in range(total_teaspoons + 1 - a): - for c in range(total_teaspoons + 1 - a - b): - teaspoons = (a, b, c, total_teaspoons - a - b - c) - - scores.append(score(ingredients, teaspoons)) - calories.append( - sum( - ingredient[-1] * teaspoon - for ingredient, teaspoon in zip(ingredients, teaspoons) - ) + ingredients: list[list[int]] = [] + for line in lines: + _, *scores = cast( + tuple[str, int, int, int, int, int], + parse.parse( # type: ignore + "{}: capacity {:d}, durability {:d}, flavor {:d}, " + "texture {:d}, calories {:d}", + line, + ), ) + ingredients.append(scores) + total_teaspoons = 100 + calories: list[int] = [] + scores: list[int] = [] -answer_1 = max(scores) -print(f"answer 1 is {answer_1}") + for a in range(total_teaspoons + 1): + for b in range(total_teaspoons + 1 - a): + for c in range(total_teaspoons + 1 - a - b): + teaspoons = (a, b, c, total_teaspoons - a - b - c) -answer_2 = max(score for score, calory in zip(scores, calories) if calory == 500) -print(f"answer 2 is {answer_2}") + scores.append(score(ingredients, teaspoons)) + calories.append( + sum( + ingredient[-1] * teaspoon + for ingredient, teaspoon in zip(ingredients, teaspoons) + ) + ) + + yield max(scores) + yield max(score for score, calory in zip(scores, calories) if calory == 500) diff --git a/src/holt59/aoc/2015/day16.py b/src/holt59/aoc/2015/day16.py index 8a3b275..e335677 100644 --- a/src/holt59/aoc/2015/day16.py +++ b/src/holt59/aoc/2015/day16.py @@ -1,8 +1,9 @@ import operator as op import re -import sys from collections import defaultdict -from typing import Callable +from typing import Any, Callable, Iterator + +from ..base import BaseSolver MFCSAM: dict[str, int] = { "children": 3, @@ -17,18 +18,10 @@ MFCSAM: dict[str, int] = { "perfumes": 1, } -lines = sys.stdin.readlines() -aunts: list[dict[str, int]] = [ - { - match[1]: int(match[2]) - for match in re.findall(R"((?P[^:, ]+): (?P\d+))", line) - } - for line in lines -] - - -def match(operators: dict[str, Callable[[int, int], bool]]) -> int: +def match( + aunts: list[dict[str, int]], operators: dict[str, Callable[[int, int], bool]] +) -> int: return next( i for i, aunt in enumerate(aunts, start=1) @@ -36,16 +29,29 @@ def match(operators: dict[str, Callable[[int, int], bool]]) -> int: ) -answer_1 = match(defaultdict(lambda: op.eq)) -print(f"answer 1 is {answer_1}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -answer_2 = match( - defaultdict( - lambda: op.eq, - trees=op.gt, - cats=op.gt, - pomeranians=op.lt, - goldfish=op.lt, - ) -) -print(f"answer 2 is {answer_2}") + aunts: list[dict[str, int]] = [ + { + match[1]: int(match[2]) + for match in re.findall( + R"((?P[^:, ]+): (?P\d+))", line + ) + } + for line in lines + ] + + yield match(aunts, defaultdict(lambda: op.eq)) + + yield match( + aunts, + defaultdict( + lambda: op.eq, + trees=op.gt, + cats=op.gt, + pomeranians=op.lt, + goldfish=op.lt, + ), + ) diff --git a/src/holt59/aoc/2015/day17.py b/src/holt59/aoc/2015/day17.py index e4704c3..c954f9b 100644 --- a/src/holt59/aoc/2015/day17.py +++ b/src/holt59/aoc/2015/day17.py @@ -1,5 +1,6 @@ -import sys -from typing import Iterator +from typing import Any, Iterator + +from ..base import BaseSolver def iter_combinations(value: int, containers: list[int]) -> Iterator[tuple[int, ...]]: @@ -16,15 +17,18 @@ def iter_combinations(value: int, containers: list[int]) -> Iterator[tuple[int, yield (containers[i],) + combination -containers = [int(c) for c in sys.stdin.read().split()] -total = 25 if len(containers) <= 5 else 150 +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + containers = [int(c) for c in input.split()] + total = 25 if len(containers) <= 5 else 150 -combinations = [combination for combination in iter_combinations(total, containers)] + combinations = [ + combination for combination in iter_combinations(total, containers) + ] -answer_1 = len(combinations) -print(f"answer 1 is {answer_1}") + yield len(combinations) -min_containers = min(len(combination) for combination in combinations) - -answer_2 = sum(1 for combination in combinations if len(combination) == min_containers) -print(f"answer 2 is {answer_2}") + min_containers = min(len(combination) for combination in combinations) + yield sum( + 1 for combination in combinations if len(combination) == min_containers + ) diff --git a/src/holt59/aoc/2015/day18.py b/src/holt59/aoc/2015/day18.py index b6de077..847b1d7 100644 --- a/src/holt59/aoc/2015/day18.py +++ b/src/holt59/aoc/2015/day18.py @@ -1,66 +1,66 @@ import itertools -import sys +from typing import Any, Iterator import numpy as np from numpy.typing import NDArray -grid0 = np.array([[c == "#" for c in line] for line in sys.stdin.read().splitlines()]) +from ..base import BaseSolver -# add an always off circle around -grid0 = np.concatenate( - [ - np.zeros((grid0.shape[0] + 2, 1), dtype=bool), - np.concatenate( + +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + grid0 = np.array([[c == "#" for c in line] for line in input.splitlines()]) + + # add an always off circle around + grid0 = np.concatenate( [ - np.zeros((1, grid0.shape[1]), dtype=bool), - grid0, - np.zeros((1, grid0.shape[1]), dtype=bool), - ] - ), - np.zeros((grid0.shape[0] + 2, 1), dtype=bool), - ], - axis=1, -) + np.zeros((grid0.shape[0] + 2, 1), dtype=bool), + np.concatenate( + [ + np.zeros((1, grid0.shape[1]), dtype=bool), + grid0, + np.zeros((1, grid0.shape[1]), dtype=bool), + ] + ), + np.zeros((grid0.shape[0] + 2, 1), dtype=bool), + ], + axis=1, + ) -moves = list(itertools.product([-1, 0, 1], repeat=2)) -moves.remove((0, 0)) + moves = list(itertools.product([-1, 0, 1], repeat=2)) + moves.remove((0, 0)) -jjs, iis = np.meshgrid( - np.arange(1, grid0.shape[0] - 1, dtype=int), - np.arange(1, grid0.shape[1] - 1, dtype=int), -) -iis, jjs = iis.flatten(), jjs.flatten() + jjs, iis = np.meshgrid( + np.arange(1, grid0.shape[0] - 1, dtype=int), + np.arange(1, grid0.shape[1] - 1, dtype=int), + ) + iis, jjs = iis.flatten(), jjs.flatten() -ins = iis[:, None] + np.array(moves)[:, 0] -jns = jjs[:, None] + np.array(moves)[:, 1] + ins = iis[:, None] + np.array(moves)[:, 0] + jns = jjs[:, None] + np.array(moves)[:, 1] + def game_of_life(grid: NDArray[np.bool_]) -> NDArray[np.bool_]: + neighbors_on = grid[ins, jns].sum(axis=1) + cells_on = grid[iis, jjs] -def game_of_life(grid: NDArray[np.bool_]) -> NDArray[np.bool_]: - neighbors_on = grid[ins, jns].sum(axis=1) - cells_on = grid[iis, jjs] + grid = np.zeros_like(grid) + grid[iis, jjs] = (neighbors_on == 3) | (cells_on & (neighbors_on == 2)) - grid = np.zeros_like(grid) - grid[iis, jjs] = (neighbors_on == 3) | (cells_on & (neighbors_on == 2)) + return grid - return grid + grid = grid0 + n_steps = 4 if len(grid) < 10 else 100 + for _ in range(n_steps): + grid = game_of_life(grid) + yield grid.sum() -grid = grid0 -n_steps = 4 if len(grid) < 10 else 100 -for _ in range(n_steps): - grid = game_of_life(grid) + n_steps = 5 if len(grid) < 10 else 100 + grid = grid0 + for _ in range(n_steps): + grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True + grid = game_of_life(grid) -answer_1 = grid.sum() -print(f"answer 1 is {answer_1}") + grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True - -n_steps = 5 if len(grid) < 10 else 100 -grid = grid0 -for _ in range(n_steps): - grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True - grid = game_of_life(grid) - -grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True - -answer_2 = sum(cell for line in grid for cell in line) -print(f"answer 2 is {answer_2}") + yield sum(cell for line in grid for cell in line) diff --git a/src/holt59/aoc/2015/day19.py b/src/holt59/aoc/2015/day19.py index db57831..6bcfb41 100644 --- a/src/holt59/aoc/2015/day19.py +++ b/src/holt59/aoc/2015/day19.py @@ -1,56 +1,58 @@ -import sys from collections import defaultdict +from typing import Any, Iterator -replacements_s, molecule = sys.stdin.read().split("\n\n") - -REPLACEMENTS: dict[str, list[str]] = defaultdict(list) -for replacement_s in replacements_s.splitlines(): - p = replacement_s.split(" => ") - REPLACEMENTS[p[0]].append(p[1]) -molecule = molecule.strip() - -generated = [ - molecule[:i] + replacement + molecule[i + len(symbol) :] - for symbol, replacements in REPLACEMENTS.items() - for replacement in replacements - for i in range(len(molecule)) - if molecule[i:].startswith(symbol) -] - -answer_1 = len(set(generated)) -print(f"answer 1 is {answer_1}") - -inversion: dict[str, str] = { - replacement: symbol - for symbol, replacements in REPLACEMENTS.items() - for replacement in replacements -} - -# there is actually only one way to create the molecule, and we can greedily replace -# tokens with their replacements, e.g., if H => OH then we can replace OH by H directly -# without thinking - -count = 0 -while molecule != "e": - i = 0 - m2 = "" - while i < len(molecule): - found = False - for replacement in inversion: - if molecule[i:].startswith(replacement): - m2 += inversion[replacement] - i += len(replacement) - count += 1 - found = True - break - - if not found: - m2 += molecule[i] - i += 1 - - # print(m2) - molecule = m2 +from ..base import BaseSolver -answer_2 = count -print(f"answer 2 is {count}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + replacements_s, molecule = input.split("\n\n") + + REPLACEMENTS: dict[str, list[str]] = defaultdict(list) + for replacement_s in replacements_s.splitlines(): + p = replacement_s.split(" => ") + REPLACEMENTS[p[0]].append(p[1]) + molecule = molecule.strip() + + generated = [ + molecule[:i] + replacement + molecule[i + len(symbol) :] + for symbol, replacements in REPLACEMENTS.items() + for replacement in replacements + for i in range(len(molecule)) + if molecule[i:].startswith(symbol) + ] + + yield len(set(generated)) + + inversion: dict[str, str] = { + replacement: symbol + for symbol, replacements in REPLACEMENTS.items() + for replacement in replacements + } + + # there is actually only one way to create the molecule, and we can greedily replace + # tokens with their replacements, e.g., if H => OH then we can replace OH by H directly + # without thinking + + count = 0 + while molecule != "e": + i = 0 + m2 = "" + while i < len(molecule): + found = False + for replacement in inversion: + if molecule[i:].startswith(replacement): + m2 += inversion[replacement] + i += len(replacement) + count += 1 + found = True + break + + if not found: + m2 += molecule[i] + i += 1 + + # print(m2) + molecule = m2 + + yield count diff --git a/src/holt59/aoc/2015/day2.py b/src/holt59/aoc/2015/day2.py index d2b8ffe..b492305 100644 --- a/src/holt59/aoc/2015/day2.py +++ b/src/holt59/aoc/2015/day2.py @@ -1,20 +1,24 @@ -import sys +from typing import Any, Iterator import numpy as np -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -length, width, height = np.array( - [[int(c) for c in line.split("x")] for line in lines] -).T -lw, wh, hl = (length * width, width * height, height * length) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + length, width, height = np.array( + [[int(c) for c in line.split("x")] for line in input.splitlines()] + ).T -answer_1 = np.sum(2 * (lw + wh + hl) + np.min(np.stack([lw, wh, hl]), axis=0)) -print(f"answer 1 is {answer_1}") + lw, wh, hl = (length * width, width * height, height * length) -answer_2 = np.sum( - length * width * height - + 2 * np.min(np.stack([length + width, length + height, height + width]), axis=0) -) -print(f"answer 2 is {answer_2}") + yield np.sum(2 * (lw + wh + hl) + np.min(np.stack([lw, wh, hl]), axis=0)) + + yield np.sum( + length * width * height + + 2 + * np.min( + np.stack([length + width, length + height, height + width]), axis=0 + ) + ) diff --git a/src/holt59/aoc/2015/day20.py b/src/holt59/aoc/2015/day20.py index 4eb3f71..3e97484 100644 --- a/src/holt59/aoc/2015/day20.py +++ b/src/holt59/aoc/2015/day20.py @@ -1,10 +1,10 @@ import itertools -import sys +from typing import Any, Iterator -target = int(sys.stdin.read()) +from ..base import BaseSolver -def presents(n: int, elf: int, max: int = target) -> int: +def presents(n: int, elf: int, max: int) -> int: count = 0 k = 1 while k * k < n: @@ -21,8 +21,9 @@ def presents(n: int, elf: int, max: int = target) -> int: return count -answer_1 = next(n for n in itertools.count(1) if presents(n, 10) >= target) -print(f"answer 1 is {answer_1}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + target = int(input) -answer_2 = next(n for n in itertools.count(1) if presents(n, 11, 50) >= target) -print(f"answer 2 is {answer_2}") + yield next(n for n in itertools.count(1) if presents(n, 10, target) >= target) + yield next(n for n in itertools.count(1) if presents(n, 11, 50) >= target) diff --git a/src/holt59/aoc/2015/day21.py b/src/holt59/aoc/2015/day21.py index cf71059..39f021b 100644 --- a/src/holt59/aoc/2015/day21.py +++ b/src/holt59/aoc/2015/day21.py @@ -1,7 +1,8 @@ import itertools -import sys from math import ceil -from typing import TypeAlias +from typing import Any, Iterator, TypeAlias + +from ..base import BaseSolver Modifier: TypeAlias = tuple[str, int, int, int] @@ -33,34 +34,31 @@ RINGS: list[Modifier] = [ ] -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -player_hp = 100 + player_hp = 100 -boss_attack = int(lines[1].split(":")[1].strip()) -boss_armor = int(lines[2].split(":")[1].strip()) -boss_hp = int(lines[0].split(":")[1].strip()) + boss_attack = int(lines[1].split(":")[1].strip()) + boss_armor = int(lines[2].split(":")[1].strip()) + boss_hp = int(lines[0].split(":")[1].strip()) + min_cost, max_cost = 1_000_000, 0 + for equipments in itertools.product(WEAPONS, ARMORS, RINGS, RINGS): + if equipments[-1][0] != "" and equipments[-2] == equipments[-1]: + continue -min_cost, max_cost = 1_000_000, 0 -for equipments in itertools.product(WEAPONS, ARMORS, RINGS, RINGS): - if equipments[-1][0] != "" and equipments[-2] == equipments[-1]: - continue + cost, player_attack, player_armor = ( + sum(equipment[1:][k] for equipment in equipments) for k in range(3) + ) - cost, player_attack, player_armor = ( - sum(equipment[1:][k] for equipment in equipments) for k in range(3) - ) + if ceil(boss_hp / max(1, player_attack - boss_armor)) <= ceil( + player_hp / max(1, boss_attack - player_armor) + ): + min_cost = min(cost, min_cost) + else: + max_cost = max(cost, max_cost) - if ceil(boss_hp / max(1, player_attack - boss_armor)) <= ceil( - player_hp / max(1, boss_attack - player_armor) - ): - min_cost = min(cost, min_cost) - else: - max_cost = max(cost, max_cost) - - -answer_1 = min_cost -print(f"answer 1 is {answer_1}") - -answer_2 = max_cost -print(f"answer 2 is {answer_2}") + yield min_cost + yield max_cost diff --git a/src/holt59/aoc/2015/day22.py b/src/holt59/aoc/2015/day22.py index 0c488ec..6d1456f 100644 --- a/src/holt59/aoc/2015/day22.py +++ b/src/holt59/aoc/2015/day22.py @@ -1,8 +1,9 @@ from __future__ import annotations import heapq -import sys -from typing import Literal, TypeAlias, cast +from typing import Any, Iterator, Literal, TypeAlias, cast + +from ..base import BaseSolver PlayerType: TypeAlias = Literal["player", "boss"] SpellType: TypeAlias = Literal["magic missile", "drain", "shield", "poison", "recharge"] @@ -62,17 +63,6 @@ def play( continue visited.add((player, player_hp, player_mana, player_armor, boss_hp, buffs)) - - if hard_mode and player == "player": - player_hp = max(0, player_hp - 1) - - if player_hp == 0: - continue - - if boss_hp == 0: - winning_node = spells - continue - new_buffs: list[tuple[BuffType, int]] = [] for buff, length in buffs: length = length - 1 @@ -88,6 +78,16 @@ def play( if length > 0: new_buffs.append((buff, length)) + if hard_mode and player == "player": + player_hp = player_hp - 1 + + if player_hp <= 0: + continue + + if boss_hp <= 0: + winning_node = spells + continue + buffs = tuple(new_buffs) if player == "boss": @@ -155,23 +155,28 @@ def play( return winning_node -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -player_hp = 50 -player_mana = 500 -player_armor = 0 + player_hp = 50 + player_mana = 500 + player_armor = 0 -boss_hp = int(lines[0].split(":")[1].strip()) -boss_attack = int(lines[1].split(":")[1].strip()) + boss_hp = int(lines[0].split(":")[1].strip()) + boss_attack = int(lines[1].split(":")[1].strip()) -answer_1 = sum( - c - for _, c in play(player_hp, player_mana, player_armor, boss_hp, boss_attack, False) -) -print(f"answer 1 is {answer_1}") + yield sum( + c + for _, c in play( + player_hp, player_mana, player_armor, boss_hp, boss_attack, False + ) + ) -# 1242 (not working) -answer_2 = sum( - c for _, c in play(player_hp, player_mana, player_armor, boss_hp, boss_attack, True) -) -print(f"answer 2 is {answer_2}") + # 1242 (not working) + yield sum( + c + for _, c in play( + player_hp, player_mana, player_armor, boss_hp, boss_attack, True + ) + ) diff --git a/src/holt59/aoc/2015/day3.py b/src/holt59/aoc/2015/day3.py index 0c376c1..73284ab 100644 --- a/src/holt59/aoc/2015/day3.py +++ b/src/holt59/aoc/2015/day3.py @@ -1,7 +1,7 @@ -import sys from collections import defaultdict +from typing import Any, Iterator -line = sys.stdin.read().strip() +from ..base import BaseSolver def process(directions: str) -> dict[tuple[int, int], int]: @@ -27,8 +27,7 @@ def process(directions: str) -> dict[tuple[int, int], int]: return counts -answer_1 = len(process(line)) -print(f"answer 1 is {answer_1}") - -answer_2 = len(process(line[::2]) | process(line[1::2])) -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + yield len(process(input)) + yield len(process(input[::2]) | process(input[1::2])) diff --git a/src/holt59/aoc/2015/day4.py b/src/holt59/aoc/2015/day4.py index 4b8efa9..e23f233 100644 --- a/src/holt59/aoc/2015/day4.py +++ b/src/holt59/aoc/2015/day4.py @@ -1,16 +1,20 @@ import hashlib import itertools -import sys +from typing import Any, Iterator -line = sys.stdin.read().strip() +from ..base import BaseSolver -it = iter(itertools.count(1)) -answer_1 = next( - i for i in it if hashlib.md5(f"{line}{i}".encode()).hexdigest().startswith("00000") -) -print(f"answer 1 is {answer_1}") -answer_2 = next( - i for i in it if hashlib.md5(f"{line}{i}".encode()).hexdigest().startswith("000000") -) -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + it = iter(itertools.count(1)) + yield next( + i + for i in it + if hashlib.md5(f"{input}{i}".encode()).hexdigest().startswith("00000") + ) + yield next( + i + for i in it + if hashlib.md5(f"{input}{i}".encode()).hexdigest().startswith("000000") + ) diff --git a/src/holt59/aoc/2015/day5.py b/src/holt59/aoc/2015/day5.py index 52fe963..7e1509e 100644 --- a/src/holt59/aoc/2015/day5.py +++ b/src/holt59/aoc/2015/day5.py @@ -1,4 +1,6 @@ -import sys +from typing import Any, Iterator + +from ..base import BaseSolver VOWELS = "aeiou" FORBIDDEN = {"ab", "cd", "pq", "xy"} @@ -27,10 +29,8 @@ def is_nice_2(s: str) -> bool: return True -lines = sys.stdin.read().splitlines() - -answer_1 = sum(map(is_nice_1, lines)) -print(f"answer 1 is {answer_1}") - -answer_2 = sum(map(is_nice_2, lines)) -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() + yield sum(map(is_nice_1, lines)) + yield sum(map(is_nice_2, lines)) diff --git a/src/holt59/aoc/2015/day6.py b/src/holt59/aoc/2015/day6.py index 1b5c21a..f46c42d 100644 --- a/src/holt59/aoc/2015/day6.py +++ b/src/holt59/aoc/2015/day6.py @@ -1,33 +1,32 @@ -import sys -from typing import Literal, cast +from typing import Any, Iterator, Literal, cast import numpy as np import parse # type: ignore -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -lights_1 = np.zeros((1000, 1000), dtype=bool) -lights_2 = np.zeros((1000, 1000), dtype=int) -for line in lines: - action, sx, sy, ex, ey = cast( - tuple[Literal["turn on", "turn off", "toggle"], int, int, int, int], - parse.parse("{} {:d},{:d} through {:d},{:d}", line), # type: ignore - ) - ex, ey = ex + 1, ey + 1 - match action: - case "turn on": - lights_1[sx:ex, sy:ey] = True - lights_2[sx:ex, sy:ey] += 1 - case "turn off": - lights_1[sx:ex, sy:ey] = False - lights_2[sx:ex, sy:ey] = np.maximum(lights_2[sx:ex, sy:ey] - 1, 0) - case "toggle": - lights_1[sx:ex, sy:ey] = ~lights_1[sx:ex, sy:ey] - lights_2[sx:ex, sy:ey] += 2 +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lights_1 = np.zeros((1000, 1000), dtype=bool) + lights_2 = np.zeros((1000, 1000), dtype=int) + for line in input.splitlines(): + action, sx, sy, ex, ey = cast( + tuple[Literal["turn on", "turn off", "toggle"], int, int, int, int], + parse.parse("{} {:d},{:d} through {:d},{:d}", line), # type: ignore + ) + ex, ey = ex + 1, ey + 1 -answer_1 = lights_1.sum() -print(f"answer 1 is {answer_1}") + match action: + case "turn on": + lights_1[sx:ex, sy:ey] = True + lights_2[sx:ex, sy:ey] += 1 + case "turn off": + lights_1[sx:ex, sy:ey] = False + lights_2[sx:ex, sy:ey] = np.maximum(lights_2[sx:ex, sy:ey] - 1, 0) + case "toggle": + lights_1[sx:ex, sy:ey] = ~lights_1[sx:ex, sy:ey] + lights_2[sx:ex, sy:ey] += 2 -answer_2 = lights_2.sum() -print(f"answer 2 is {answer_2}") + yield lights_1.sum() + yield lights_2.sum() diff --git a/src/holt59/aoc/2015/day7.py b/src/holt59/aoc/2015/day7.py index a6e3fe5..b45ce6a 100644 --- a/src/holt59/aoc/2015/day7.py +++ b/src/holt59/aoc/2015/day7.py @@ -1,11 +1,7 @@ -import logging import operator -import os -import sys -from typing import Callable +from typing import Any, Callable, Iterator -VERBOSE = os.getenv("AOC_VERBOSE") == "True" -logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING) +from ..base import BaseSolver OPERATORS = { "AND": operator.and_, @@ -36,48 +32,6 @@ def value_of(key: str) -> tuple[str, Callable[[dict[str, int]], int]]: return key, lambda values: values[key] -lines = sys.stdin.read().splitlines() - -signals: Signals = {} -values: dict[str, int] = {"": 0} - -for line in lines: - command, signal = line.split(" -> ") - - if command.startswith("NOT"): - name = command.split(" ")[1] - signals[signal] = ( - (name, ""), - (lambda values, _n=name: values[_n], lambda _v: 0), - lambda a, _b: ~a, - ) - - elif not any(command.find(name) >= 0 for name in OPERATORS): - try: - values[signal] = int(command) - except ValueError: - signals[signal] = ( - (command, ""), - (lambda values, _c=command: values[_c], lambda _v: 0), - lambda a, _b: a, - ) - - else: - op: Callable[[int, int], int] = zero_op - lhs_s, rhs_s = "", "" - - for name in OPERATORS: - if command.find(name) >= 0: - op = OPERATORS[name] - lhs_s, rhs_s = command.split(f" {name} ") - break - - lhs_s, lhs_fn = value_of(lhs_s) - rhs_s, rhs_fn = value_of(rhs_s) - - signals[signal] = ((lhs_s, rhs_s), (lhs_fn, rhs_fn), op) - - def process( signals: Signals, values: dict[str, int], @@ -91,11 +45,52 @@ def process( return values -values_1 = process(signals.copy(), values.copy()) -logging.info("\n" + "\n".join(f"{k}: {values_1[k]}" for k in sorted(values_1))) -answer_1 = values_1["a"] -print(f"answer 1 is {answer_1}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any] | None: + lines = input.splitlines() -values_2 = process(signals.copy(), values | {"b": values_1["a"]}) -answer_2 = values_2["a"] -print(f"answer 2 is {answer_2}") + signals: Signals = {} + values: dict[str, int] = {"": 0} + + for line in lines: + command, signal = line.split(" -> ") + + if command.startswith("NOT"): + name = command.split(" ")[1] + signals[signal] = ( + (name, ""), + (lambda values, _n=name: values[_n], lambda _v: 0), + lambda a, _b: ~a, + ) + + elif not any(command.find(name) >= 0 for name in OPERATORS): + try: + values[signal] = int(command) + except ValueError: + signals[signal] = ( + (command, ""), + (lambda values, _c=command: values[_c], lambda _v: 0), + lambda a, _b: a, + ) + + else: + op: Callable[[int, int], int] = zero_op + lhs_s, rhs_s = "", "" + + for name in OPERATORS: + if command.find(name) >= 0: + op = OPERATORS[name] + lhs_s, rhs_s = command.split(f" {name} ") + break + + lhs_s, lhs_fn = value_of(lhs_s) + rhs_s, rhs_fn = value_of(rhs_s) + + signals[signal] = ((lhs_s, rhs_s), (lhs_fn, rhs_fn), op) + + values_1 = process(signals.copy(), values.copy()) + for k in sorted(values_1): + self.logger.info(f"{k}: {values_1[k]}") + yield values_1["a"] + + yield process(signals.copy(), values | {"b": values_1["a"]})["a"] diff --git a/src/holt59/aoc/2015/day8.py b/src/holt59/aoc/2015/day8.py index be22c22..d6662ad 100644 --- a/src/holt59/aoc/2015/day8.py +++ b/src/holt59/aoc/2015/day8.py @@ -1,35 +1,32 @@ -import logging -import os -import sys +from typing import Any, Iterator -VERBOSE = os.getenv("AOC_VERBOSE") == "True" -logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING) +from ..base import BaseSolver -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -answer_1 = sum( - # left and right quotes (not in memory) - 2 - # each \\ adds one character in the literals (compared to memory) - + line.count(R"\\") - # each \" adds one character in the literals (compared to memory) - + line[1:-1].count(R"\"") - # each \xFF adds 3 characters in the literals (compared to memory), but we must not - # count A\\x (A != \), but we must count A\\\x (A != \) - in practice we should also - # avoid \\\\x, etc., but this does not occur in the examples and the actual input - + 3 * (line.count(R"\x") - line.count(R"\\x") + line.count(R"\\\x")) - for line in lines -) -print(f"answer 1 is {answer_1}") + yield sum( + # left and right quotes (not in memory) + 2 + # each \\ adds one character in the literals (compared to memory) + + line.count(R"\\") + # each \" adds one character in the literals (compared to memory) + + line[1:-1].count(R"\"") + # each \xFF adds 3 characters in the literals (compared to memory), but we must not + # count A\\x (A != \), but we must count A\\\x (A != \) - in practice we should also + # avoid \\\\x, etc., but this does not occur in the examples and the actual input + + 3 * (line.count(R"\x") - line.count(R"\\x") + line.count(R"\\\x")) + for line in lines + ) -answer_2 = sum( - # needs to wrap in quotes (2 characters) - 2 - # needs to escape every \ with an extra \ - + line.count("\\") - # needs to escape every " with an extra \ (including the first and last ones) - + line.count('"') - for line in lines -) -print(f"answer 2 is {answer_2}") + yield sum( + # needs to wrap in quotes (2 characters) + 2 + # needs to escape every \ with an extra \ + + line.count("\\") + # needs to escape every " with an extra \ (including the first and last ones) + + line.count('"') + for line in lines + ) diff --git a/src/holt59/aoc/2015/day9.py b/src/holt59/aoc/2015/day9.py index f6e7176..e055964 100644 --- a/src/holt59/aoc/2015/day9.py +++ b/src/holt59/aoc/2015/day9.py @@ -1,27 +1,28 @@ import itertools -import sys from collections import defaultdict -from typing import cast +from typing import Any, Iterator, cast import parse # type: ignore -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -distances: dict[str, dict[str, int]] = defaultdict(dict) -for line in lines: - origin, destination, length = cast( - tuple[str, str, int], - parse.parse("{} to {} = {:d}", line), # type: ignore - ) - distances[origin][destination] = distances[destination][origin] = length -distance_of_routes = { - route: sum(distances[o][d] for o, d in zip(route[:-1], route[1:])) - for route in map(tuple, itertools.permutations(distances)) -} +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -answer_1 = min(distance_of_routes.values()) -print(f"answer 1 is {answer_1}") + distances: dict[str, dict[str, int]] = defaultdict(dict) + for line in lines: + origin, destination, length = cast( + tuple[str, str, int], + parse.parse("{} to {} = {:d}", line), # type: ignore + ) + distances[origin][destination] = distances[destination][origin] = length -answer_2 = max(distance_of_routes.values()) -print(f"answer 2 is {answer_2}") + distance_of_routes = { + route: sum(distances[o][d] for o, d in zip(route[:-1], route[1:])) + for route in map(tuple, itertools.permutations(distances)) + } + + yield min(distance_of_routes.values()) + yield max(distance_of_routes.values()) diff --git a/src/holt59/aoc/2021/day1.py b/src/holt59/aoc/2021/day1.py index 3fa54b5..4f405e7 100644 --- a/src/holt59/aoc/2021/day1.py +++ b/src/holt59/aoc/2021/day1.py @@ -1,14 +1,17 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -values = [int(line) for line in lines] -# part 1 -answer_1 = sum(v2 > v1 for v1, v2 in zip(values[:-1], values[1:])) -print(f"answer 1 is {answer_1}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -# part 2 -runnings = [sum(values[i : i + 3]) for i in range(len(values) - 2)] -answer_2 = sum(v2 > v1 for v1, v2 in zip(runnings[:-1], runnings[1:])) -print(f"answer 2 is {answer_2}") + values = [int(line) for line in lines] + + # part 1 + yield sum(v2 > v1 for v1, v2 in zip(values[:-1], values[1:])) + + # part 2 + runnings = [sum(values[i : i + 3]) for i in range(len(values) - 2)] + yield sum(v2 > v1 for v1, v2 in zip(runnings[:-1], runnings[1:])) diff --git a/src/holt59/aoc/2021/day10.py b/src/holt59/aoc/2021/day10.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day10.py +++ b/src/holt59/aoc/2021/day10.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day11.py b/src/holt59/aoc/2021/day11.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day11.py +++ b/src/holt59/aoc/2021/day11.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day12.py b/src/holt59/aoc/2021/day12.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day12.py +++ b/src/holt59/aoc/2021/day12.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day13.py b/src/holt59/aoc/2021/day13.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day13.py +++ b/src/holt59/aoc/2021/day13.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day14.py b/src/holt59/aoc/2021/day14.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day14.py +++ b/src/holt59/aoc/2021/day14.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day15.py b/src/holt59/aoc/2021/day15.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day15.py +++ b/src/holt59/aoc/2021/day15.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day16.py b/src/holt59/aoc/2021/day16.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day16.py +++ b/src/holt59/aoc/2021/day16.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day17.py b/src/holt59/aoc/2021/day17.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day17.py +++ b/src/holt59/aoc/2021/day17.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day18.py b/src/holt59/aoc/2021/day18.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day18.py +++ b/src/holt59/aoc/2021/day18.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day19.py b/src/holt59/aoc/2021/day19.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day19.py +++ b/src/holt59/aoc/2021/day19.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day2.py b/src/holt59/aoc/2021/day2.py index 6a8db61..bc1a5c8 100644 --- a/src/holt59/aoc/2021/day2.py +++ b/src/holt59/aoc/2021/day2.py @@ -1,41 +1,38 @@ -import sys from math import prod -from typing import Literal, TypeAlias, cast +from typing import Any, Iterator, Literal, TypeAlias, cast -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver Command: TypeAlias = Literal["forward", "up", "down"] -commands: list[tuple[Command, int]] = [ - (cast(Command, (p := line.split())[0]), int(p[1])) for line in lines -] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -def depth_and_position(use_aim: bool): - aim, pos, depth = 0, 0, 0 - for command, value in commands: - d_depth = 0 - match command: - case "forward": - pos += value - depth += value * aim - case "up": - d_depth = -value - case "down": - d_depth = value + commands: list[tuple[Command, int]] = [ + (cast(Command, (p := line.split())[0]), int(p[1])) for line in lines + ] - if use_aim: - aim += d_depth - else: - depth += value + def depth_and_position(use_aim: bool): + aim, pos, depth = 0, 0, 0 + for command, value in commands: + d_depth = 0 + match command: + case "forward": + pos += value + depth += value * aim + case "up": + d_depth = -value + case "down": + d_depth = value - return depth, pos + if use_aim: + aim += d_depth + else: + depth += value + return depth, pos -# part 1 -answer_1 = prod(depth_and_position(False)) -print(f"answer 1 is {answer_1}") - -# part 2 -answer_2 = prod(depth_and_position(True)) -print(f"answer 2 is {answer_2}") + yield prod(depth_and_position(False)) + yield prod(depth_and_position(True)) diff --git a/src/holt59/aoc/2021/day20.py b/src/holt59/aoc/2021/day20.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day20.py +++ b/src/holt59/aoc/2021/day20.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day21.py b/src/holt59/aoc/2021/day21.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day21.py +++ b/src/holt59/aoc/2021/day21.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day22.py b/src/holt59/aoc/2021/day22.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day22.py +++ b/src/holt59/aoc/2021/day22.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day23.py b/src/holt59/aoc/2021/day23.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day23.py +++ b/src/holt59/aoc/2021/day23.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day24.py b/src/holt59/aoc/2021/day24.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day24.py +++ b/src/holt59/aoc/2021/day24.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day25.py b/src/holt59/aoc/2021/day25.py index dbaa94d..07e201e 100644 --- a/src/holt59/aoc/2021/day25.py +++ b/src/holt59/aoc/2021/day25.py @@ -1,11 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -# part 1 -answer_1 = ... -print(f"answer 1 is {answer_1}") -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2021/day3.py b/src/holt59/aoc/2021/day3.py index 9b21ba4..c19232a 100644 --- a/src/holt59/aoc/2021/day3.py +++ b/src/holt59/aoc/2021/day3.py @@ -1,6 +1,7 @@ -import sys from collections import Counter -from typing import Literal +from typing import Any, Iterator, Literal + +from ..base import BaseSolver def generator_rating( @@ -20,20 +21,23 @@ def generator_rating( return values[0] -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() + # part 1 + most_and_least_common = [ + tuple( + Counter(line[col] for line in lines).most_common(2)[m][0] + for m in range(2) + ) + for col in range(len(lines[0])) + ] + gamma_rate = int("".join(most for most, _ in most_and_least_common), base=2) + epsilon_rate = int("".join(least for _, least in most_and_least_common), base=2) + yield gamma_rate * epsilon_rate -# part 1 -most_and_least_common = [ - tuple(Counter(line[col] for line in lines).most_common(2)[m][0] for m in range(2)) - for col in range(len(lines[0])) -] -gamma_rate = int("".join(most for most, _ in most_and_least_common), base=2) -epsilon_rate = int("".join(least for _, least in most_and_least_common), base=2) -print(f"answer 1 is {gamma_rate * epsilon_rate}") - -# part 2 -oxygen_generator_rating = int(generator_rating(lines, True, "1"), base=2) -co2_scrubber_rating = int(generator_rating(lines, False, "0"), base=2) -answer_2 = oxygen_generator_rating * co2_scrubber_rating -print(f"answer 2 is {answer_2}") + # part 2 + oxygen_generator_rating = int(generator_rating(lines, True, "1"), base=2) + co2_scrubber_rating = int(generator_rating(lines, False, "0"), base=2) + yield oxygen_generator_rating * co2_scrubber_rating diff --git a/src/holt59/aoc/2021/day4.py b/src/holt59/aoc/2021/day4.py index 2e86096..df97649 100644 --- a/src/holt59/aoc/2021/day4.py +++ b/src/holt59/aoc/2021/day4.py @@ -1,45 +1,52 @@ -import sys +from typing import Any, Iterator import numpy as np -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -numbers = [int(c) for c in lines[0].split(",")] -boards = np.asarray( - [ - [[int(c) for c in line.split()] for line in lines[start : start + 5]] - for start in range(2, len(lines), 6) - ] -) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -# (round, score) for each board (-1 when not found) -winning_rounds: list[tuple[int, int]] = [(-1, -1) for _ in range(len(boards))] -marked = np.zeros_like(boards, dtype=bool) + numbers = [int(c) for c in lines[0].split(",")] -for round, number in enumerate(numbers): - # mark boards - marked[boards == number] = True + boards = np.asarray( + [ + [[int(c) for c in line.split()] for line in lines[start : start + 5]] + for start in range(2, len(lines), 6) + ] + ) - # check each board for winning - for index in range(len(boards)): - if winning_rounds[index][0] > 0: - continue + # (round, score) for each board (-1 when not found) + winning_rounds: list[tuple[int, int]] = [(-1, -1) for _ in range(len(boards))] + marked = np.zeros_like(boards, dtype=bool) - if np.any(np.all(marked[index], axis=0) | np.all(marked[index], axis=1)): - winning_rounds[index] = ( - round, - number * int(np.sum(boards[index][~marked[index]])), - ) + for round, number in enumerate(numbers): + # mark boards + marked[boards == number] = True - # all boards are winning - break - if np.all(marked.all(axis=1) | marked.all(axis=2)): - break + # check each board for winning + for index in range(len(boards)): + if winning_rounds[index][0] > 0: + continue -# part 1 -(_, score) = min(winning_rounds, key=lambda w: w[0]) -print(f"answer 1 is {score}") + if np.any( + np.all(marked[index], axis=0) | np.all(marked[index], axis=1) + ): + winning_rounds[index] = ( + round, + number * int(np.sum(boards[index][~marked[index]])), + ) -# part 2 -(_, score) = max(winning_rounds, key=lambda w: w[0]) -print(f"answer 2 is {score}") + # all boards are winning - break + if np.all(marked.all(axis=1) | marked.all(axis=2)): + break + + # part 1 + (_, score) = min(winning_rounds, key=lambda w: w[0]) + yield score + + # part 2 + (_, score) = max(winning_rounds, key=lambda w: w[0]) + yield score diff --git a/src/holt59/aoc/2021/day5.py b/src/holt59/aoc/2021/day5.py index 5e75518..5c99204 100644 --- a/src/holt59/aoc/2021/day5.py +++ b/src/holt59/aoc/2021/day5.py @@ -1,48 +1,48 @@ -import sys +from typing import Any, Iterator import numpy as np -lines: list[str] = sys.stdin.read().splitlines() +from ..base import BaseSolver -sections: list[tuple[tuple[int, int], tuple[int, int]]] = [ - ( - ( - int(line.split(" -> ")[0].split(",")[0]), - int(line.split(" -> ")[0].split(",")[1]), - ), - ( - int(line.split(" -> ")[1].split(",")[0]), - int(line.split(" -> ")[1].split(",")[1]), - ), - ) - for line in lines -] -np_sections = np.array(sections).reshape(-1, 4) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -x_min, x_max, y_min, y_max = ( - min(np_sections[:, 0].min(), np_sections[:, 2].min()), - max(np_sections[:, 0].max(), np_sections[:, 2].max()), - min(np_sections[:, 1].min(), np_sections[:, 3].min()), - max(np_sections[:, 1].max(), np_sections[:, 3].max()), -) + sections: list[tuple[tuple[int, int], tuple[int, int]]] = [ + ( + ( + int(line.split(" -> ")[0].split(",")[0]), + int(line.split(" -> ")[0].split(",")[1]), + ), + ( + int(line.split(" -> ")[1].split(",")[0]), + int(line.split(" -> ")[1].split(",")[1]), + ), + ) + for line in lines + ] -counts_1 = np.zeros((y_max + 1, x_max + 1), dtype=int) -counts_2 = counts_1.copy() + np_sections = np.array(sections).reshape(-1, 4) -for (x1, y1), (x2, y2) in sections: - x_rng = range(x1, x2 + 1, 1) if x2 >= x1 else range(x1, x2 - 1, -1) - y_rng = range(y1, y2 + 1, 1) if y2 >= y1 else range(y1, y2 - 1, -1) + x_max, y_max = ( + max(np_sections[:, 0].max(), np_sections[:, 2].max()), + max(np_sections[:, 1].max(), np_sections[:, 3].max()), + ) - if x1 == x2 or y1 == y2: - counts_1[list(y_rng), list(x_rng)] += 1 - counts_2[list(y_rng), list(x_rng)] += 1 - elif abs(x2 - x1) == abs(y2 - y1): - for i, j in zip(y_rng, x_rng): - counts_2[i, j] += 1 + counts_1 = np.zeros((y_max + 1, x_max + 1), dtype=int) + counts_2 = counts_1.copy() -answer_1 = (counts_1 >= 2).sum() -print(f"answer 1 is {answer_1}") + for (x1, y1), (x2, y2) in sections: + x_rng = range(x1, x2 + 1, 1) if x2 >= x1 else range(x1, x2 - 1, -1) + y_rng = range(y1, y2 + 1, 1) if y2 >= y1 else range(y1, y2 - 1, -1) -answer_2 = (counts_2 >= 2).sum() -print(f"answer 2 is {answer_2}") + if x1 == x2 or y1 == y2: + counts_1[list(y_rng), list(x_rng)] += 1 + counts_2[list(y_rng), list(x_rng)] += 1 + elif abs(x2 - x1) == abs(y2 - y1): + for i, j in zip(y_rng, x_rng): + counts_2[i, j] += 1 + + yield (counts_1 >= 2).sum() + yield (counts_2 >= 2).sum() diff --git a/src/holt59/aoc/2021/day6.py b/src/holt59/aoc/2021/day6.py index 5847186..573042b 100644 --- a/src/holt59/aoc/2021/day6.py +++ b/src/holt59/aoc/2021/day6.py @@ -1,21 +1,21 @@ -import sys +from typing import Any, Iterator -values = [int(c) for c in sys.stdin.read().strip().split(",")] +from ..base import BaseSolver -days = 256 -lanterns = {day: 0 for day in range(days)} -for value in values: - for day in range(value, days, 7): - lanterns[day] += 1 -for day in range(days): - for day2 in range(day + 9, days, 7): - lanterns[day2] += lanterns[day] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + values = [int(c) for c in input.split(",")] -# part 1 -answer_1 = sum(v for k, v in lanterns.items() if k < 80) + len(values) -print(f"answer 1 is {answer_1}") + days = 256 + lanterns = {day: 0 for day in range(days)} + for value in values: + for day in range(value, days, 7): + lanterns[day] += 1 -# part 2 -answer_2 = sum(lanterns.values()) + len(values) -print(f"answer 2 is {answer_2}") + for day in range(days): + for day2 in range(day + 9, days, 7): + lanterns[day2] += lanterns[day] + + yield sum(v for k, v in lanterns.items() if k < 80) + len(values) + yield sum(lanterns.values()) + len(values) diff --git a/src/holt59/aoc/2021/day7.py b/src/holt59/aoc/2021/day7.py index 2532edb..1092d2d 100644 --- a/src/holt59/aoc/2021/day7.py +++ b/src/holt59/aoc/2021/day7.py @@ -1,19 +1,22 @@ -import sys +from typing import Any, Iterator -positions = [int(c) for c in sys.stdin.read().strip().split(",")] +from ..base import BaseSolver -min_position, max_position = min(positions), max(positions) -# part 1 -answer_1 = min( - sum(abs(p - position) for p in positions) - for position in range(min_position, max_position + 1) -) -print(f"answer 1 is {answer_1}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + positions = [int(c) for c in input.split(",")] -# part 2 -answer_2 = min( - sum(abs(p - position) * (abs(p - position) + 1) // 2 for p in positions) - for position in range(min_position, max_position + 1) -) -print(f"answer 2 is {answer_2}") + min_position, max_position = min(positions), max(positions) + + # part 1 + yield min( + sum(abs(p - position) for p in positions) + for position in range(min_position, max_position + 1) + ) + + # part 2 + yield min( + sum(abs(p - position) * (abs(p - position) + 1) // 2 for p in positions) + for position in range(min_position, max_position + 1) + ) diff --git a/src/holt59/aoc/2021/day8.py b/src/holt59/aoc/2021/day8.py index 1560410..16d502d 100644 --- a/src/holt59/aoc/2021/day8.py +++ b/src/holt59/aoc/2021/day8.py @@ -1,8 +1,7 @@ import itertools -import os -import sys +from typing import Any, Iterator -VERBOSE = os.getenv("AOC_VERBOSE") == "True" +from ..base import BaseSolver digits = { "abcefg": 0, @@ -17,71 +16,74 @@ digits = { "abcdfg": 9, } -lines = sys.stdin.read().splitlines() -# part 1 -lengths = {len(k) for k, v in digits.items() if v in (1, 4, 7, 8)} -answer_1 = sum( - len(p) in lengths for line in lines for p in line.split("|")[1].strip().split() -) -print(f"answer 1 is {answer_1}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -# part 2 -values: list[int] = [] + # part 1 + lengths = {len(k) for k, v in digits.items() if v in (1, 4, 7, 8)} + yield sum( + len(p) in lengths + for line in lines + for p in line.split("|")[1].strip().split() + ) -for line in lines: - parts = line.split("|") - broken_digits = sorted(parts[0].strip().split(), key=len) + # part 2 + values: list[int] = [] - per_length = { - k: list(v) - for k, v in itertools.groupby(sorted(broken_digits, key=len), key=len) - } + for line in lines: + parts = line.split("|") + broken_digits = sorted(parts[0].strip().split(), key=len) - # a can be found immediately - a = next(u for u in per_length[3][0] if u not in per_length[2][0]) + per_length = { + k: list(v) + for k, v in itertools.groupby(sorted(broken_digits, key=len), key=len) + } - # c and f have only two possible values corresponding to the single entry of - # length 2 - cf = list(per_length[2][0]) + # a can be found immediately + a = next(u for u in per_length[3][0] if u not in per_length[2][0]) - # the only digit of length 4 contains bcdf, so we can deduce bd by removing cf - bd = [u for u in per_length[4][0] if u not in cf] + # c and f have only two possible values corresponding to the single entry of + # length 2 + cf = list(per_length[2][0]) - # the 3 digits of length 5 have a, d and g in common - adg = [u for u in per_length[5][0] if all(u in pe for pe in per_length[5][1:])] + # the only digit of length 4 contains bcdf, so we can deduce bd by removing cf + bd = [u for u in per_length[4][0] if u not in cf] - # we can remove a - dg = [u for u in adg if u != a] + # the 3 digits of length 5 have a, d and g in common + adg = [ + u for u in per_length[5][0] if all(u in pe for pe in per_length[5][1:]) + ] - # we can deduce d and g - d = next(u for u in dg if u in bd) - g = next(u for u in dg if u != d) + # we can remove a + dg = [u for u in adg if u != a] - # then b - b = next(u for u in bd if u != d) + # we can deduce d and g + d = next(u for u in dg if u in bd) + g = next(u for u in dg if u != d) - # f is in the three 6-length digits, while c is only in 2 - f = next(u for u in cf if all(u in p for p in per_length[6])) + # then b + b = next(u for u in bd if u != d) - # c is not f - c = next(u for u in cf if u != f) + # f is in the three 6-length digits, while c is only in 2 + f = next(u for u in cf if all(u in p for p in per_length[6])) - # e is the last one - e = next(u for u in "abcdefg" if u not in {a, b, c, d, f, g}) + # c is not f + c = next(u for u in cf if u != f) - mapping = dict(zip((a, b, c, d, e, f, g), "abcdefg")) + # e is the last one + e = next(u for u in "abcdefg" if u not in {a, b, c, d, f, g}) - value = 0 - for number in parts[1].strip().split(): - digit = "".join(sorted(mapping[c] for c in number)) - value = 10 * value + digits[digit] + mapping = dict(zip((a, b, c, d, e, f, g), "abcdefg")) - if VERBOSE: - print(value) + value = 0 + for number in parts[1].strip().split(): + digit = "".join(sorted(mapping[c] for c in number)) + value = 10 * value + digits[digit] - values.append(value) + self.logger.info(f"value for '{line}' is {value}") + values.append(value) -answer_2 = sum(values) -print(f"answer 2 is {answer_2}") + yield sum(values) diff --git a/src/holt59/aoc/2021/day9.py b/src/holt59/aoc/2021/day9.py index 850131d..fb1f064 100644 --- a/src/holt59/aoc/2021/day9.py +++ b/src/holt59/aoc/2021/day9.py @@ -1,18 +1,18 @@ -import sys from math import prod +from typing import Any, Iterator -values = [[int(c) for c in row] for row in sys.stdin.read().splitlines()] -n_rows, n_cols = len(values), len(values[0]) +from ..base import BaseSolver -def neighbors(point: tuple[int, int]): +def neighbors(point: tuple[int, int], n_rows: int, n_cols: int): i, j = point for di, dj in ((-1, 0), (+1, 0), (0, -1), (0, +1)): if 0 <= i + di < n_rows and 0 <= j + dj < n_cols: yield (i + di, j + dj) -def basin(start: tuple[int, int]) -> set[tuple[int, int]]: +def basin(values: list[list[int]], start: tuple[int, int]) -> set[tuple[int, int]]: + n_rows, n_cols = len(values), len(values[0]) visited: set[tuple[int, int]] = set() queue = [start] @@ -23,22 +23,25 @@ def basin(start: tuple[int, int]) -> set[tuple[int, int]]: continue visited.add((i, j)) - queue.extend(neighbors((i, j))) + queue.extend(neighbors((i, j), n_rows, n_cols)) return visited -low_points = [ - (i, j) - for i in range(n_rows) - for j in range(n_cols) - if all(values[ti][tj] > values[i][j] for ti, tj in neighbors((i, j))) -] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + values = [[int(c) for c in row] for row in input.splitlines()] + n_rows, n_cols = len(values), len(values[0]) -# part 1 -answer_1 = sum(values[i][j] + 1 for i, j in low_points) -print(f"answer 1 is {answer_1}") + low_points = [ + (i, j) + for i in range(n_rows) + for j in range(n_cols) + if all( + values[ti][tj] > values[i][j] + for ti, tj in neighbors((i, j), n_rows, n_cols) + ) + ] -# part 2 -answer_2 = prod(sorted(len(basin(point)) for point in low_points)[-3:]) -print(f"answer 2 is {answer_2}") + yield sum(values[i][j] + 1 for i, j in low_points) + yield prod(sorted(len(basin(values, point)) for point in low_points)[-3:]) diff --git a/src/holt59/aoc/2022/day1.py b/src/holt59/aoc/2022/day1.py index 4dc115c..17776a8 100644 --- a/src/holt59/aoc/2022/day1.py +++ b/src/holt59/aoc/2022/day1.py @@ -1,7 +1,12 @@ -import sys +from typing import Any, Iterator -blocks = sys.stdin.read().split("\n\n") -values = sorted(sum(map(int, block.split())) for block in blocks) +from ..base import BaseSolver -print(f"answer 1 is {values[-1]}") -print(f"answer 2 is {sum(values[-3:])}") + +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + blocks = input.split("\n\n") + values = sorted(sum(map(int, block.split())) for block in blocks) + + yield values[-1] + yield sum(values[-3:]) diff --git a/src/holt59/aoc/2022/day10.py b/src/holt59/aoc/2022/day10.py index 73470c6..f06442e 100644 --- a/src/holt59/aoc/2022/day10.py +++ b/src/holt59/aoc/2022/day10.py @@ -1,38 +1,43 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() - -cycle = 1 -x = 1 - -values = {cycle: x} - -for line in lines: - cycle += 1 - - if line == "noop": - pass - else: - r = int(line.split()[1]) - - values[cycle] = x - - cycle += 1 - x += r - - values[cycle] = x - -answer_1 = sum(c * values[c] for c in range(20, max(values.keys()) + 1, 40)) -print(f"answer 1 is {answer_1}") +from ..base import BaseSolver -for i in range(6): - for j in range(40): - v = values[1 + i * 40 + j] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] - if j >= v - 1 and j <= v + 1: - print("#", end="") - else: - print(".", end="") + cycle, x = 1, 1 + values = {cycle: x} - print() + for line in lines: + cycle += 1 + + if line == "noop": + pass + else: + r = int(line.split()[1]) + + values[cycle] = x + + cycle += 1 + x += r + + values[cycle] = x + + answer_1 = sum(c * values[c] for c in range(20, max(values.keys()) + 1, 40)) + yield answer_1 + + yield ( + "\n" + + "\n".join( + "".join( + "#" + if j >= (v := values[1 + i * 40 + j]) - 1 and j <= v + 1 + else "." + for j in range(40) + ) + for i in range(6) + ) + + "\n" + ) diff --git a/src/holt59/aoc/2022/day11.py b/src/holt59/aoc/2022/day11.py index 5c2efd9..a505d71 100644 --- a/src/holt59/aoc/2022/day11.py +++ b/src/holt59/aoc/2022/day11.py @@ -1,7 +1,8 @@ import copy -import sys from functools import reduce -from typing import Callable, Final, Mapping, Sequence +from typing import Any, Callable, Final, Iterator, Mapping, Sequence + +from ..base import BaseSolver class Monkey: @@ -119,24 +120,28 @@ def monkey_business(inspects: dict[Monkey, int]) -> int: return sorted_levels[-2] * sorted_levels[-1] -monkeys = [parse_monkey(block.splitlines()) for block in sys.stdin.read().split("\n\n")] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + monkeys = [parse_monkey(block.splitlines()) for block in input.split("\n\n")] -# case 1: we simply divide the worry by 3 after applying the monkey worry operation -answer_1 = monkey_business( - run(copy.deepcopy(monkeys), 20, me_worry_fn=lambda w: w // 3) -) -print(f"answer 1 is {answer_1}") + # case 1: we simply divide the worry by 3 after applying the monkey worry operation + yield monkey_business( + run(copy.deepcopy(monkeys), 20, me_worry_fn=lambda w: w // 3) + ) -# case 2: to keep reasonable level values, we can use a modulo operation, we need to -# use the product of all "divisible by" test so that the test remains valid -# -# (a + b) % c == ((a % c) + (b % c)) % c --- this would work for a single test value -# -# (a + b) % c == ((a % d) + (b % d)) % c --- if d is a multiple of c, which is why here -# we use the product of all test value -# -total_test_value = reduce(lambda w, m: w * m.test_value, monkeys, 1) -answer_2 = monkey_business( - run(copy.deepcopy(monkeys), 10_000, me_worry_fn=lambda w: w % total_test_value) -) -print(f"answer 2 is {answer_2}") + # case 2: to keep reasonable level values, we can use a modulo operation, we need to + # use the product of all "divisible by" test so that the test remains valid + # + # (a + b) % c == ((a % c) + (b % c)) % c --- this would work for a single test value + # + # (a + b) % c == ((a % d) + (b % d)) % c --- if d is a multiple of c, which is why here + # we use the product of all test value + # + total_test_value = reduce(lambda w, m: w * m.test_value, monkeys, 1) + yield monkey_business( + run( + copy.deepcopy(monkeys), + 10_000, + me_worry_fn=lambda w: w % total_test_value, + ) + ) diff --git a/src/holt59/aoc/2022/day12.py b/src/holt59/aoc/2022/day12.py index 35a7116..f4811a0 100644 --- a/src/holt59/aoc/2022/day12.py +++ b/src/holt59/aoc/2022/day12.py @@ -1,6 +1,7 @@ import heapq -import sys -from typing import Callable, Iterator, TypeVar +from typing import Any, Callable, Iterator, TypeVar + +from ..base import BaseSolver Node = TypeVar("Node") @@ -68,30 +69,6 @@ def make_path(parents: dict[Node, Node], start: Node, end: Node) -> list[Node] | return list(reversed(path)) -def print_path(path: list[tuple[int, int]], n_rows: int, n_cols: int) -> None: - end = path[-1] - - graph = [["." for _c in range(n_cols)] for _r in range(n_rows)] - graph[end[0]][end[1]] = "E" - - for i in range(0, len(path) - 1): - cr, cc = path[i] - nr, nc = path[i + 1] - - if cr == nr and nc == cc - 1: - graph[cr][cc] = "<" - elif cr == nr and nc == cc + 1: - graph[cr][cc] = ">" - elif cr == nr - 1 and nc == cc: - graph[cr][cc] = "v" - elif cr == nr + 1 and nc == cc: - graph[cr][cc] = "^" - else: - assert False, "{} -> {} infeasible".format(path[i], path[i + 1]) - - print("\n".join("".join(row) for row in graph)) - - def neighbors( grid: list[list[int]], node: tuple[int, int], up: bool ) -> Iterator[tuple[int, int]]: @@ -118,46 +95,74 @@ def neighbors( # === main code === -lines = sys.stdin.read().splitlines() -grid = [[ord(cell) - ord("a") for cell in line] for line in lines] +class Solver(BaseSolver): + def print_path(self, path: list[tuple[int, int]], n_rows: int, n_cols: int) -> None: + end = path[-1] -start: tuple[int, int] | None = None -end: tuple[int, int] | None = None + graph = [["." for _c in range(n_cols)] for _r in range(n_rows)] + graph[end[0]][end[1]] = "E" -# for part 2 -start_s: list[tuple[int, int]] = [] + for i in range(0, len(path) - 1): + cr, cc = path[i] + nr, nc = path[i + 1] -for i_row, row in enumerate(grid): - for i_col, col in enumerate(row): - if chr(col + ord("a")) == "S": - start = (i_row, i_col) - start_s.append(start) - elif chr(col + ord("a")) == "E": - end = (i_row, i_col) - elif col == 0: - start_s.append((i_row, i_col)) + if cr == nr and nc == cc - 1: + graph[cr][cc] = "<" + elif cr == nr and nc == cc + 1: + graph[cr][cc] = ">" + elif cr == nr - 1 and nc == cc: + graph[cr][cc] = "v" + elif cr == nr + 1 and nc == cc: + graph[cr][cc] = "^" + else: + assert False, "{} -> {} infeasible".format(path[i], path[i + 1]) -assert start is not None -assert end is not None + for row in graph: + self.logger.info("".join(row)) -# fix values -grid[start[0]][start[1]] = 0 -grid[end[0]][end[1]] = ord("z") - ord("a") + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() + grid = [[ord(cell) - ord("a") for cell in line] for line in lines] -lengths_1, parents_1 = dijkstra( - start=start, neighbors=lambda n: neighbors(grid, n, True), cost=lambda lhs, rhs: 1 -) -path_1 = make_path(parents_1, start, end) -assert path_1 is not None + start: tuple[int, int] | None = None + end: tuple[int, int] | None = None -print_path(path_1, n_rows=len(grid), n_cols=len(grid[0])) + # for part 2 + start_s: list[tuple[int, int]] = [] -print(f"answer 1 is {lengths_1[end] - 1}") + for i_row, row in enumerate(grid): + for i_col, col in enumerate(row): + if chr(col + ord("a")) == "S": + start = (i_row, i_col) + start_s.append(start) + elif chr(col + ord("a")) == "E": + end = (i_row, i_col) + elif col == 0: + start_s.append((i_row, i_col)) -lengths_2, parents_2 = dijkstra( - start=end, neighbors=lambda n: neighbors(grid, n, False), cost=lambda lhs, rhs: 1 -) -answer_2 = min(lengths_2.get(start, float("inf")) for start in start_s) -print(f"answer 2 is {answer_2}") + assert start is not None + assert end is not None + + # fix values + grid[start[0]][start[1]] = 0 + grid[end[0]][end[1]] = ord("z") - ord("a") + + lengths_1, parents_1 = dijkstra( + start=start, + neighbors=lambda n: neighbors(grid, n, True), + cost=lambda lhs, rhs: 1, + ) + path_1 = make_path(parents_1, start, end) + assert path_1 is not None + + self.print_path(path_1, n_rows=len(grid), n_cols=len(grid[0])) + yield lengths_1[end] - 1 + + lengths_2, _ = dijkstra( + start=end, + neighbors=lambda n: neighbors(grid, n, False), + cost=lambda lhs, rhs: 1, + ) + yield min(lengths_2.get(start, float("inf")) for start in start_s) diff --git a/src/holt59/aoc/2022/day13.py b/src/holt59/aoc/2022/day13.py index c9d2361..e2c5d72 100644 --- a/src/holt59/aoc/2022/day13.py +++ b/src/holt59/aoc/2022/day13.py @@ -1,11 +1,8 @@ import json -import sys from functools import cmp_to_key -from typing import TypeAlias, cast +from typing import Any, Iterator, TypeAlias, cast -blocks = sys.stdin.read().strip().split("\n\n") - -pairs = [tuple(json.loads(p) for p in block.split("\n")) for block in blocks] +from ..base import BaseSolver Packet: TypeAlias = list[int | list["Packet"]] @@ -28,14 +25,18 @@ def compare(lhs: Packet, rhs: Packet) -> int: return len(rhs) - len(lhs) -answer_1 = sum(i + 1 for i, (lhs, rhs) in enumerate(pairs) if compare(lhs, rhs) > 0) -print(f"answer_1 is {answer_1}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + blocks = input.split("\n\n") + pairs = [tuple(json.loads(p) for p in block.split("\n")) for block in blocks] -dividers = [[[2]], [[6]]] + yield sum(i + 1 for i, (lhs, rhs) in enumerate(pairs) if compare(lhs, rhs) > 0) -packets = [packet for packets in pairs for packet in packets] -packets.extend(dividers) -packets = list(reversed(sorted(packets, key=cmp_to_key(compare)))) + dividers = [[[2]], [[6]]] -d_index = [packets.index(d) + 1 for d in dividers] -print(f"answer 2 is {d_index[0] * d_index[1]}") + packets = [packet for packets in pairs for packet in packets] + packets.extend(dividers) + packets = list(reversed(sorted(packets, key=cmp_to_key(compare)))) + + d_index = [packets.index(d) + 1 for d in dividers] + yield d_index[0] * d_index[1] diff --git a/src/holt59/aoc/2022/day14.py b/src/holt59/aoc/2022/day14.py index 17d937c..4603157 100644 --- a/src/holt59/aoc/2022/day14.py +++ b/src/holt59/aoc/2022/day14.py @@ -1,6 +1,7 @@ -import sys from enum import Enum, auto -from typing import Callable, cast +from typing import Any, Callable, Iterator, cast + +from ..base import BaseSolver class Cell(Enum): @@ -12,26 +13,6 @@ class Cell(Enum): return {Cell.AIR: ".", Cell.ROCK: "#", Cell.SAND: "O"}[self] -def print_blocks(blocks: dict[tuple[int, int], Cell]): - """ - Print the given set of blocks on a grid. - - Args: - blocks: Set of blocks to print. - """ - x_min, y_min, x_max, y_max = ( - min(x for x, _ in blocks), - 0, - max(x for x, _ in blocks), - max(y for _, y in blocks), - ) - - for y in range(y_min, y_max + 1): - print( - "".join(str(blocks.get((x, y), Cell.AIR)) for x in range(x_min, x_max + 1)) - ) - - def flow( blocks: dict[tuple[int, int], Cell], stop_fn: Callable[[int, int], bool], @@ -84,57 +65,75 @@ def flow( # === inputs === -lines = sys.stdin.read().splitlines() -paths: list[list[tuple[int, int]]] = [] -for line in lines: - parts = line.split(" -> ") - paths.append( - [ - cast(tuple[int, int], tuple(int(c.strip()) for c in part.split(","))) - for part in parts - ] - ) +class Solver(BaseSolver): + def print_blocks(self, blocks: dict[tuple[int, int], Cell]): + """ + Print the given set of blocks on a grid. + Args: + blocks: Set of blocks to print. + """ + x_min, y_min, x_max, y_max = ( + min(x for x, _ in blocks), + 0, + max(x for x, _ in blocks), + max(y for _, y in blocks), + ) -blocks: dict[tuple[int, int], Cell] = {} -for path in paths: - for start, end in zip(path[:-1], path[1:]): - x_start = min(start[0], end[0]) - x_end = max(start[0], end[0]) + 1 - y_start = min(start[1], end[1]) - y_end = max(start[1], end[1]) + 1 + for y in range(y_min, y_max + 1): + self.logger.info( + "".join( + str(blocks.get((x, y), Cell.AIR)) for x in range(x_min, x_max + 1) + ) + ) - for x in range(x_start, x_end): - for y in range(y_start, y_end): - blocks[x, y] = Cell.ROCK + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] -print_blocks(blocks) -print() + paths: list[list[tuple[int, int]]] = [] + for line in lines: + parts = line.split(" -> ") + paths.append( + [ + cast( + tuple[int, int], tuple(int(c.strip()) for c in part.split(",")) + ) + for part in parts + ] + ) -x_min, y_min, x_max, y_max = ( - min(x for x, _ in blocks), - 0, - max(x for x, _ in blocks), - max(y for _, y in blocks), -) + blocks: dict[tuple[int, int], Cell] = {} + for path in paths: + for start, end in zip(path[:-1], path[1:]): + x_start = min(start[0], end[0]) + x_end = max(start[0], end[0]) + 1 + y_start = min(start[1], end[1]) + y_end = max(start[1], end[1]) + 1 -# === part 1 === + for x in range(x_start, x_end): + for y in range(y_start, y_end): + blocks[x, y] = Cell.ROCK -blocks_1 = flow( - blocks.copy(), stop_fn=lambda x, y: y > y_max, fill_fn=lambda x, y: Cell.AIR -) -print_blocks(blocks_1) -print(f"answer 1 is {sum(v == Cell.SAND for v in blocks_1.values())}") -print() + self.print_blocks(blocks) -# === part 2 === + y_max = max(y for _, y in blocks) -blocks_2 = flow( - blocks.copy(), - stop_fn=lambda x, y: x == 500 and y == 0, - fill_fn=lambda x, y: Cell.AIR if y < y_max + 2 else Cell.ROCK, -) -blocks_2[500, 0] = Cell.SAND -print_blocks(blocks_2) -print(f"answer 2 is {sum(v == Cell.SAND for v in blocks_2.values())}") + # === part 1 === + + blocks_1 = flow( + blocks.copy(), stop_fn=lambda x, y: y > y_max, fill_fn=lambda x, y: Cell.AIR + ) + self.print_blocks(blocks_1) + yield sum(v == Cell.SAND for v in blocks_1.values()) + + # === part 2 === + + blocks_2 = flow( + blocks.copy(), + stop_fn=lambda x, y: x == 500 and y == 0, + fill_fn=lambda x, y: Cell.AIR if y < y_max + 2 else Cell.ROCK, + ) + blocks_2[500, 0] = Cell.SAND + self.print_blocks(blocks_2) + yield sum(v == Cell.SAND for v in blocks_2.values()) diff --git a/src/holt59/aoc/2022/day15.py b/src/holt59/aoc/2022/day15.py index ba537aa..ac9bba8 100644 --- a/src/holt59/aoc/2022/day15.py +++ b/src/holt59/aoc/2022/day15.py @@ -1,90 +1,95 @@ -import sys -from typing import Any +import itertools as it +from typing import Any, Iterator import numpy as np import parse # type: ignore from numpy.typing import NDArray - -def part1(sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], row: int) -> int: - no_beacons_row_l: list[NDArray[np.floating[Any]]] = [] - - for (sx, sy), (bx, by) in sensor_to_beacon.items(): - d = abs(sx - bx) + abs(sy - by) # closest - - no_beacons_row_l.append(sx - np.arange(0, d - abs(sy - row) + 1)) # type: ignore - no_beacons_row_l.append(sx + np.arange(0, d - abs(sy - row) + 1)) # type: ignore - - beacons_at_row = set(bx for (bx, by) in sensor_to_beacon.values() if by == row) - no_beacons_row = set(np.concatenate(no_beacons_row_l)).difference(beacons_at_row) # type: ignore - - return len(no_beacons_row) +from ..base import BaseSolver -def part2_intervals( - sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int -) -> tuple[int, int, int]: - from tqdm import trange +class Solver(BaseSolver): + def part1( + self, sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], row: int + ) -> int: + no_beacons_row_l: list[NDArray[np.floating[Any]]] = [] + + for (sx, sy), (bx, by) in sensor_to_beacon.items(): + d = abs(sx - bx) + abs(sy - by) # closest + + no_beacons_row_l.append(sx - np.arange(0, d - abs(sy - row) + 1)) # type: ignore + no_beacons_row_l.append(sx + np.arange(0, d - abs(sy - row) + 1)) # type: ignore + + beacons_at_row = set(bx for (bx, by) in sensor_to_beacon.values() if by == row) + no_beacons_row = set(it.chain(*no_beacons_row_l)).difference(beacons_at_row) # type: ignore + + return len(no_beacons_row) + + def part2_intervals( + self, sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int + ) -> tuple[int, int, int]: + for y in self.progress.wrap(range(xy_max + 1)): + its: list[tuple[int, int]] = [] + for (sx, sy), (bx, by) in sensor_to_beacon.items(): + d = abs(sx - bx) + abs(sy - by) + dx = d - abs(sy - y) + + if dx >= 0: + its.append((max(0, sx - dx), min(sx + dx, xy_max))) + + its = sorted(its) + _, e = its[0] + + for si, ei in its[1:]: + if si > e + 1: + return si - 1, y, 4_000_000 * (si - 1) + y + if ei > e: + e = ei + + return (0, 0, 0) + + def part2_cplex( + self, sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int + ) -> tuple[int, int, int]: + from docplex.mp.model import Model + + m = Model() + + x, y = m.continuous_var_list(2, ub=xy_max, name=["x", "y"]) - for y in trange(xy_max + 1): - its: list[tuple[int, int]] = [] for (sx, sy), (bx, by) in sensor_to_beacon.items(): d = abs(sx - bx) + abs(sy - by) - dx = d - abs(sy - y) + m.add_constraint( + m.abs(x - sx) + m.abs(y - sy) >= d + 1, # type: ignore + ctname=f"ct_{sx}_{sy}", + ) - if dx >= 0: - its.append((max(0, sx - dx), min(sx + dx, xy_max))) + m.set_objective("min", x + y) - its = sorted(its) - _, e = its[0] + s = m.solve() + assert s is not None - for si, ei in its[1:]: - if si > e + 1: - return si - 1, y, 4_000_000 * (si - 1) + y - if ei > e: - e = ei + vx = int(s.get_value(x)) + vy = int(s.get_value(y)) + return vx, vy, 4_000_000 * vx + vy - return (0, 0, 0) + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() + sensor_to_beacon: dict[tuple[int, int], tuple[int, int]] = {} -def part2_cplex( - sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int -) -> tuple[int, int, int]: - from docplex.mp.model import Model + for line in lines: + r: dict[str, str] = parse.parse( # type: ignore + "Sensor at x={sx}, y={sy}: closest beacon is at x={bx}, y={by}", line + ) + sensor_to_beacon[int(r["sx"]), int(r["sy"])] = (int(r["bx"]), int(r["by"])) - m = Model() + xy_max = 4_000_000 if max(sensor_to_beacon) > (1_000, 0) else 20 + row = 2_000_000 if max(sensor_to_beacon) > (1_000, 0) else 10 - x, y = m.continuous_var_list(2, ub=xy_max, name=["x", "y"]) + yield self.part1(sensor_to_beacon, row) - for (sx, sy), (bx, by) in sensor_to_beacon.items(): - d = abs(sx - bx) + abs(sy - by) - m.add_constraint(m.abs(x - sx) + m.abs(y - sy) >= d + 1, ctname=f"ct_{sx}_{sy}") # type: ignore - - m.set_objective("min", x + y) - - s = m.solve() - assert s is not None - - vx = int(s.get_value(x)) - vy = int(s.get_value(y)) - return vx, vy, 4_000_000 * vx + vy - - -lines = sys.stdin.read().splitlines() - -sensor_to_beacon: dict[tuple[int, int], tuple[int, int]] = {} - -for line in lines: - r: dict[str, str] = parse.parse( # type: ignore - "Sensor at x={sx}, y={sy}: closest beacon is at x={bx}, y={by}", line - ) - sensor_to_beacon[int(r["sx"]), int(r["sy"])] = (int(r["bx"]), int(r["by"])) - -xy_max = 4_000_000 if max(sensor_to_beacon) > (1_000, 0) else 20 -row = 2_000_000 if max(sensor_to_beacon) > (1_000, 0) else 10 - -print(f"answer 1 is {part1(sensor_to_beacon, row)}") - -# x, y, a2 = part2_cplex(sensor_to_beacon, xy_max) -x, y, a2 = part2_intervals(sensor_to_beacon, xy_max) -print(f"answer 2 is {a2} (x={x}, y={y})") + # x, y, a2 = part2_cplex(sensor_to_beacon, xy_max) + x, y, a2 = self.part2_intervals(sensor_to_beacon, xy_max) + self.logger.info(f"answer 2 is {a2} (x={x}, y={y})") + yield a2 diff --git a/src/holt59/aoc/2022/day16.py b/src/holt59/aoc/2022/day16.py index 055ed25..f9068a5 100644 --- a/src/holt59/aoc/2022/day16.py +++ b/src/holt59/aoc/2022/day16.py @@ -3,12 +3,13 @@ from __future__ import annotations import heapq import itertools import re -import sys from collections import defaultdict -from typing import FrozenSet, NamedTuple +from typing import Any, FrozenSet, Iterator, NamedTuple from tqdm import tqdm +from ..base import BaseSolver + class Pipe(NamedTuple): name: str @@ -36,8 +37,8 @@ def breadth_first_search(pipes: dict[str, Pipe], pipe: Pipe) -> dict[Pipe, int]: Runs a BFS from the given pipe and return the shortest distance (in term of hops) to all other pipes. """ - queue = [(0, pipe_1)] - visited = set() + queue = [(0, pipe)] + visited: set[Pipe] = set() distances: dict[Pipe, int] = {} while len(distances) < len(pipes): @@ -122,37 +123,37 @@ def part_2( # === MAIN === -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] + pipes: dict[str, Pipe] = {} + for line in lines: + r = re.match( + R"Valve ([A-Z]+) has flow rate=([0-9]+); tunnels? leads? to valves? (.+)", + line, + ) + assert r -pipes: dict[str, Pipe] = {} -for line in lines: - r = re.match( - R"Valve ([A-Z]+) has flow rate=([0-9]+); tunnels? leads? to valves? (.+)", - line, - ) - assert r + g = r.groups() - g = r.groups() + pipes[g[0]] = Pipe(g[0], int(g[1]), g[2].split(", ")) - pipes[g[0]] = Pipe(g[0], int(g[1]), g[2].split(", ")) + # compute distances from one valve to any other + distances: dict[tuple[Pipe, Pipe], int] = {} + for pipe_1 in pipes.values(): + distances.update( + { + (pipe_1, pipe_2): distance + for pipe_2, distance in breadth_first_search(pipes, pipe_1).items() + } + ) -# compute distances from one valve to any other -distances: dict[tuple[Pipe, Pipe], int] = {} -for pipe_1 in pipes.values(): - distances.update( - { - (pipe_1, pipe_2): distance - for pipe_2, distance in breadth_first_search(pipes, pipe_1).items() - } - ) + # valves with flow + relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0) -# valves with flow -relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0) + # 1651, 1653 + yield part_1(pipes["AA"], 30, distances, relevant_pipes) - -# 1651, 1653 -print(part_1(pipes["AA"], 30, distances, relevant_pipes)) - -# 1707, 2223 -print(part_2(pipes["AA"], 26, distances, relevant_pipes)) + # 1707, 2223 + yield part_2(pipes["AA"], 26, distances, relevant_pipes) diff --git a/src/holt59/aoc/2022/day17.py b/src/holt59/aoc/2022/day17.py index 6f02c26..4d22d78 100644 --- a/src/holt59/aoc/2022/day17.py +++ b/src/holt59/aoc/2022/day17.py @@ -1,12 +1,16 @@ -import sys -from typing import Sequence, TypeVar +from typing import Any, Iterator, Sequence, TypeAlias, TypeVar import numpy as np +from numpy.typing import NDArray + +from ..base import BaseSolver T = TypeVar("T") +Tower: TypeAlias = NDArray[np.bool] -def print_tower(tower: np.ndarray, out: str = "#"): + +def print_tower(tower: Tower, out: str = "#"): print("-" * (tower.shape[1] + 2)) non_empty = False for row in reversed(range(1, tower.shape[0])): @@ -17,7 +21,7 @@ def print_tower(tower: np.ndarray, out: str = "#"): print("+" + "-" * tower.shape[1] + "+") -def tower_height(tower: np.ndarray) -> int: +def tower_height(tower: Tower) -> int: return int(tower.shape[0] - tower[::-1, :].argmax(axis=0).min() - 1) @@ -45,8 +49,8 @@ def build_tower( n_rocks: int, jets: str, early_stop: bool = False, - init: np.ndarray = np.ones(WIDTH, dtype=bool), -) -> tuple[np.ndarray, int, int, dict[int, int]]: + init: Tower = np.ones(WIDTH, dtype=bool), +) -> tuple[Tower, int, int, dict[int, int]]: tower = EMPTY_BLOCKS.copy() tower[0, :] = init @@ -95,26 +99,24 @@ def build_tower( return tower, rock_count, done_at.get((i_rock, i_jet), -1), heights -line = sys.stdin.read().strip() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + tower, *_ = build_tower(2022, input) + yield tower_height(tower) -tower, *_ = build_tower(2022, line) -answer_1 = tower_height(tower) -print(f"answer 1 is {answer_1}") + TOTAL_ROCKS = 1_000_000_000_000 + _tower_1, n_rocks_1, prev_1, heights_1 = build_tower(TOTAL_ROCKS, input, True) + assert prev_1 > 0 -TOTAL_ROCKS = 1_000_000_000_000 -tower_1, n_rocks_1, prev_1, heights_1 = build_tower(TOTAL_ROCKS, line, True) -assert prev_1 > 0 + # 2767 1513 + remaining_rocks = TOTAL_ROCKS - n_rocks_1 + n_repeat_rocks = n_rocks_1 - prev_1 + n_repeat_towers = remaining_rocks // n_repeat_rocks -# 2767 1513 -remaining_rocks = TOTAL_ROCKS - n_rocks_1 -n_repeat_rocks = n_rocks_1 - prev_1 -n_repeat_towers = remaining_rocks // n_repeat_rocks + base_height = heights_1[prev_1] + repeat_height = heights_1[prev_1 + n_repeat_rocks - 1] - heights_1[prev_1] + remaining_height = ( + heights_1[prev_1 + remaining_rocks % n_repeat_rocks] - heights_1[prev_1] + ) -base_height = heights_1[prev_1] -repeat_height = heights_1[prev_1 + n_repeat_rocks - 1] - heights_1[prev_1] -remaining_height = ( - heights_1[prev_1 + remaining_rocks % n_repeat_rocks] - heights_1[prev_1] -) - -answer_2 = base_height + (n_repeat_towers + 1) * repeat_height + remaining_height -print(f"answer 2 is {answer_2}") + yield base_height + (n_repeat_towers + 1) * repeat_height + remaining_height diff --git a/src/holt59/aoc/2022/day18.py b/src/holt59/aoc/2022/day18.py index c8d0f9a..4a93b3b 100644 --- a/src/holt59/aoc/2022/day18.py +++ b/src/holt59/aoc/2022/day18.py @@ -1,50 +1,58 @@ -import sys +from typing import Any, Iterator import numpy as np -xyz = np.asarray( - [ - tuple(int(x) for x in row.split(",")) # type: ignore - for row in sys.stdin.read().splitlines() - ] -) +from ..base import BaseSolver -xyz = xyz - xyz.min(axis=0) + 1 -cubes = np.zeros(xyz.max(axis=0) + 3, dtype=bool) -cubes[xyz[:, 0], xyz[:, 1], xyz[:, 2]] = True +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + xyz = np.asarray( + [ + tuple(int(x) for x in row.split(",")) # type: ignore + for row in input.splitlines() + ] + ) -n_dims = len(cubes.shape) + xyz = xyz - xyz.min(axis=0) + 1 -faces = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)] + cubes = np.zeros(xyz.max(axis=0) + 3, dtype=bool) + cubes[xyz[:, 0], xyz[:, 1], xyz[:, 2]] = True -answer_1 = sum( - 1 for x, y, z in xyz for dx, dy, dz in faces if not cubes[x + dx, y + dy, z + dz] -) -print(f"answer 1 is {answer_1}") + faces = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)] -visited = np.zeros_like(cubes, dtype=bool) -queue = [(0, 0, 0)] + yield sum( + 1 + for x, y, z in xyz + for dx, dy, dz in faces + if not cubes[x + dx, y + dy, z + dz] + ) -n_faces = 0 -while queue: - x, y, z = queue.pop(0) + visited = np.zeros_like(cubes, dtype=bool) + queue = [(0, 0, 0)] - if visited[x, y, z]: - continue + n_faces = 0 + while queue: + x, y, z = queue.pop(0) - visited[x, y, z] = True + if visited[x, y, z]: + continue - for dx, dy, dz in faces: - nx, ny, nz = x + dx, y + dy, z + dz - if not all(n >= 0 and n < cubes.shape[i] for i, n in enumerate((nx, ny, nz))): - continue + visited[x, y, z] = True - if visited[nx, ny, nz]: - continue + for dx, dy, dz in faces: + nx, ny, nz = x + dx, y + dy, z + dz + if not all( + n >= 0 and n < cubes.shape[i] for i, n in enumerate((nx, ny, nz)) + ): + continue - if cubes[nx, ny, nz]: - n_faces += 1 - else: - queue.append((nx, ny, nz)) -print(f"answer 2 is {n_faces}") + if visited[nx, ny, nz]: + continue + + if cubes[nx, ny, nz]: + n_faces += 1 + else: + queue.append((nx, ny, nz)) + + yield n_faces diff --git a/src/holt59/aoc/2022/day19.py b/src/holt59/aoc/2022/day19.py index 7d888fa..ef4d6bc 100644 --- a/src/holt59/aoc/2022/day19.py +++ b/src/holt59/aoc/2022/day19.py @@ -1,10 +1,11 @@ -import sys -from typing import Any, Literal +from typing import Any, Iterator, Literal import numpy as np import parse # pyright: ignore[reportMissingTypeStubs] from numpy.typing import NDArray +from ..base import BaseSolver + Reagent = Literal["ore", "clay", "obsidian", "geode"] REAGENTS: tuple[Reagent, ...] = ( "ore", @@ -62,29 +63,6 @@ def dominates(lhs: State, rhs: State): ) -lines = sys.stdin.read().splitlines() - -blueprints: list[dict[Reagent, IntOfReagent]] = [] -for line in lines: - r: list[int] = parse.parse( # type: ignore - "Blueprint {}: " - "Each ore robot costs {:d} ore. " - "Each clay robot costs {:d} ore. " - "Each obsidian robot costs {:d} ore and {:d} clay. " - "Each geode robot costs {:d} ore and {:d} obsidian.", - line, - ) - - blueprints.append( - { - "ore": {"ore": r[1]}, - "clay": {"ore": r[2]}, - "obsidian": {"ore": r[3], "clay": r[4]}, - "geode": {"ore": r[5], "obsidian": r[6]}, - } - ) - - def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int: # since we can only build one robot per time, we do not need more than X robots # of type K where X is the maximum number of K required among all robots, e.g., @@ -173,11 +151,31 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int: return max(state.reagents["geode"] for state in state_after_t[max_time]) -answer_1 = sum( - (i_blueprint + 1) * run(blueprint, 24) - for i_blueprint, blueprint in enumerate(blueprints) -) -print(f"answer 1 is {answer_1}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + blueprints: list[dict[Reagent, IntOfReagent]] = [] + for line in input.splitlines(): + r: list[int] = parse.parse( # type: ignore + "Blueprint {}: " + "Each ore robot costs {:d} ore. " + "Each clay robot costs {:d} ore. " + "Each obsidian robot costs {:d} ore and {:d} clay. " + "Each geode robot costs {:d} ore and {:d} obsidian.", + line, + ) -answer_2 = run(blueprints[0], 32) * run(blueprints[1], 32) * run(blueprints[2], 32) -print(f"answer 2 is {answer_2}") + blueprints.append( + { + "ore": {"ore": r[1]}, + "clay": {"ore": r[2]}, + "obsidian": {"ore": r[3], "clay": r[4]}, + "geode": {"ore": r[5], "obsidian": r[6]}, + } + ) + + yield sum( + (i_blueprint + 1) * run(blueprint, 24) + for i_blueprint, blueprint in enumerate(blueprints) + ) + + yield (run(blueprints[0], 32) * run(blueprints[1], 32) * run(blueprints[2], 32)) diff --git a/src/holt59/aoc/2022/day2.py b/src/holt59/aoc/2022/day2.py index 4d23c7c..c78b283 100644 --- a/src/holt59/aoc/2022/day2.py +++ b/src/holt59/aoc/2022/day2.py @@ -1,4 +1,6 @@ -import sys +from typing import Any, Iterator + +from ..base import BaseSolver def score_1(ux: int, vx: int) -> int: @@ -33,21 +35,23 @@ def score_2(ux: int, vx: int) -> int: return (ux + vx - 1) % 3 + 1 + vx * 3 -lines = sys.stdin.readlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -# the solution relies on replacing rock / paper / scissor by values 0 / 1 / 2 and using -# modulo-3 arithmetic -# -# in modulo-3 arithmetic, the winning move is 1 + the opponent move (e.g., winning move -# if opponent plays 0 is 1, or 0 if opponent plays 2 (0 = (2 + 1 % 3))) -# + # the solution relies on replacing rock / paper / scissor by values 0 / 1 / 2 and using + # modulo-3 arithmetic + # + # in modulo-3 arithmetic, the winning move is 1 + the opponent move (e.g., winning move + # if opponent plays 0 is 1, or 0 if opponent plays 2 (0 = (2 + 1 % 3))) + # -# we read the lines in a Nx2 in array with value 0/1/2 instead of A/B/C or X/Y/Z for -# easier manipulation -values = [(ord(row[0]) - ord("A"), ord(row[2]) - ord("X")) for row in lines] + # we read the lines in a Nx2 in array with value 0/1/2 instead of A/B/C or X/Y/Z for + # easier manipulation + values = [(ord(row[0]) - ord("A"), ord(row[2]) - ord("X")) for row in lines] -# part 1 - 13526 -print(f"answer 1 is {sum(score_1(*v) for v in values)}") + # part 1 - 13526 + yield sum(score_1(*v) for v in values) -# part 2 - 14204 -print(f"answer 2 is {sum(score_2(*v) for v in values)}") + # part 2 - 14204 + yield sum(score_2(*v) for v in values) diff --git a/src/holt59/aoc/2022/day20.py b/src/holt59/aoc/2022/day20.py index 5448d0a..8c74c35 100644 --- a/src/holt59/aoc/2022/day20.py +++ b/src/holt59/aoc/2022/day20.py @@ -1,6 +1,8 @@ from __future__ import annotations -import sys +from typing import Any, Iterator + +from ..base import BaseSolver class Number: @@ -65,10 +67,9 @@ def decrypt(numbers: list[Number], key: int, rounds: int) -> int: ) -numbers = [Number(int(x)) for i, x in enumerate(sys.stdin.readlines())] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + numbers = [Number(int(x)) for x in input.splitlines()] -answer_1 = decrypt(numbers, 1, 1) -print(f"answer 1 is {answer_1}") - -answer_2 = decrypt(numbers, 811589153, 10) -print(f"answer 2 is {answer_2}") + yield decrypt(numbers, 1, 1) + yield decrypt(numbers, 811589153, 10) diff --git a/src/holt59/aoc/2022/day21.py b/src/holt59/aoc/2022/day21.py index c5b3736..db9dd47 100644 --- a/src/holt59/aoc/2022/day21.py +++ b/src/holt59/aoc/2022/day21.py @@ -1,6 +1,7 @@ import operator -import sys -from typing import Callable +from typing import Any, Callable, Iterator + +from ..base import BaseSolver def compute(monkeys: dict[str, int | tuple[str, str, str]], monkey: str) -> int: @@ -77,31 +78,31 @@ def invert( return monkeys -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] -monkeys: dict[str, int | tuple[str, str, str]] = {} + monkeys: dict[str, int | tuple[str, str, str]] = {} -op_monkeys: set[str] = set() + op_monkeys: set[str] = set() -for line in lines: - parts = line.split(":") - name = parts[0].strip() + for line in lines: + parts = line.split(":") + name = parts[0].strip() - try: - value = int(parts[1].strip()) - monkeys[name] = value - except ValueError: - op1, ope, op2 = parts[1].strip().split() - monkeys[name] = (op1, ope, op2) + try: + value = int(parts[1].strip()) + monkeys[name] = value + except ValueError: + op1, ope, op2 = parts[1].strip().split() + monkeys[name] = (op1, ope, op2) - op_monkeys.add(name) + op_monkeys.add(name) + yield compute(monkeys.copy(), "root") -answer_1 = compute(monkeys.copy(), "root") -print(f"answer 1 is {answer_1}") - -# assume the second operand of 'root' can be computed, and the first one depends on -# humn, which is the case is my input and the test input -p1, _, p2 = monkeys["root"] # type: ignore -answer_2 = compute(invert(monkeys, "humn", compute(monkeys.copy(), p2)), "humn") -print(f"answer 2 is {answer_2}") + # assume the second operand of 'root' can be computed, and the first one depends on + # humn, which is the case is my input and the test input + assert isinstance(monkeys["root"], tuple) + p1, _, p2 = monkeys["root"] # type: ignore + yield compute(invert(monkeys, "humn", compute(monkeys.copy(), p2)), "humn") diff --git a/src/holt59/aoc/2022/day22.py b/src/holt59/aoc/2022/day22.py index 571f314..510791b 100644 --- a/src/holt59/aoc/2022/day22.py +++ b/src/holt59/aoc/2022/day22.py @@ -1,223 +1,243 @@ import re -import sys -from typing import Callable +from typing import Any, Callable, Iterator import numpy as np +from ..base import BaseSolver + VOID, EMPTY, WALL = 0, 1, 2 TILE_FROM_CHAR = {" ": VOID, ".": EMPTY, "#": WALL} SCORES = {"E": 0, "S": 1, "W": 2, "N": 3} -board_map_s, direction_s = sys.stdin.read().split("\n\n") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + board_map_s, direction_s = input.split("\n\n") -# board -board_lines = board_map_s.splitlines() -max_line = max(len(line) for line in board_lines) -board = np.array( - [ - [TILE_FROM_CHAR[c] for c in row] + [VOID] * (max_line - len(row)) - for row in board_map_s.splitlines() - ] -) + # board + board_lines = board_map_s.splitlines() + max_line = max(len(line) for line in board_lines) + board = np.array( + [ + [TILE_FROM_CHAR[c] for c in row] + [VOID] * (max_line - len(row)) + for row in board_map_s.splitlines() + ] + ) -directions = [ - int(p1) if p2 else p1 for p1, p2 in re.findall(R"(([0-9])+|L|R)", direction_s) -] + directions = [ + int(p1) if p2 else p1 + for p1, p2 in re.findall(R"(([0-9])+|L|R)", direction_s) + ] + # find on each row and column the first and last non-void + row_first_non_void = np.argmax(board != VOID, axis=1) + row_last_non_void = ( + board.shape[1] - np.argmax(board[:, ::-1] != VOID, axis=1) - 1 + ) + col_first_non_void = np.argmax(board != VOID, axis=0) + col_last_non_void = ( + board.shape[0] - np.argmax(board[::-1, :] != VOID, axis=0) - 1 + ) -# find on each row and column the first and last non-void -row_first_non_void = np.argmax(board != VOID, axis=1) -row_last_non_void = board.shape[1] - np.argmax(board[:, ::-1] != VOID, axis=1) - 1 -col_first_non_void = np.argmax(board != VOID, axis=0) -col_last_non_void = board.shape[0] - np.argmax(board[::-1, :] != VOID, axis=0) - 1 + faces = np.zeros_like(board) + size = np.gcd(board.shape[0], board.shape[1]) + for row in range(0, board.shape[0], size): + for col in range(row_first_non_void[row], row_last_non_void[row], size): + faces[row : row + size, col : col + size] = faces.max() + 1 + SIZE = np.gcd(*board.shape) -faces = np.zeros_like(board) -size = np.gcd(board.shape[0], board.shape[1]) -for row in range(0, board.shape[0], size): - for col in range(row_first_non_void[row], row_last_non_void[row], size): - faces[row : row + size, col : col + size] = faces.max() + 1 + # TODO: deduce this from the actual cube... + faces_wrap: dict[int, dict[str, Callable[[int, int], tuple[int, int, str]]]] -SIZE = np.gcd(*board.shape) + if board.shape == (12, 16): # example + faces_wrap = { + 1: { + "W": lambda y, x: (4, 4 + y, "S"), # 3N + "N": lambda y, x: (4, 11 - x, "S"), # 2N + "E": lambda y, x: (11 - y, 15, "W"), # 6E + }, + 2: { + "W": lambda y, x: (11, 19 - y, "N"), # 6S + "N": lambda y, x: (0, 11 - y, "S"), # 1N + "S": lambda y, x: (11, 11 - x, "N"), # 5S + }, + 3: { + "N": lambda y, x: (x - 4, 8, "E"), # 1W + "S": lambda y, x: (15 - x, 8, "E"), # 5W + }, + 4: {"E": lambda y, x: (8, 19 - y, "S")}, # 6N + 5: { + "W": lambda y, x: (7, 15 - y, "N"), # 3S + "S": lambda y, x: (7, 11 - x, "N"), # 2S + }, + 6: { + "N": lambda y, x: (19 - x, 11, "W"), # 4E + "E": lambda y, x: (11 - y, 11, "W"), # 1E + "S": lambda y, x: (19 - x, 0, "E"), # 2W + }, + } -# TODO: deduce this from the actual cube... -faces_wrap: dict[int, dict[str, Callable[[int, int], tuple[int, int, str]]]] - -if board.shape == (12, 16): # example - faces_wrap = { - 1: { - "W": lambda y, x: (4, 4 + y, "S"), # 3N - "N": lambda y, x: (4, 11 - x, "S"), # 2N - "E": lambda y, x: (11 - y, 15, "W"), # 6E - }, - 2: { - "W": lambda y, x: (11, 19 - y, "N"), # 6S - "N": lambda y, x: (0, 11 - y, "S"), # 1N - "S": lambda y, x: (11, 11 - x, "N"), # 5S - }, - 3: { - "N": lambda y, x: (x - 4, 8, "E"), # 1W - "S": lambda y, x: (15 - x, 8, "E"), # 5W - }, - 4: {"E": lambda y, x: (8, 19 - y, "S")}, # 6N - 5: { - "W": lambda y, x: (7, 15 - y, "N"), # 3S - "S": lambda y, x: (7, 11 - x, "N"), # 2S - }, - 6: { - "N": lambda y, x: (19 - x, 11, "W"), # 4E - "E": lambda y, x: (11 - y, 11, "W"), # 1E - "S": lambda y, x: (19 - x, 0, "E"), # 2W - }, - } - -else: - faces_wrap = { - 1: { - "W": lambda y, x: (3 * SIZE - y - 1, 0, "E"), # 4W - "N": lambda y, x: (2 * SIZE + x, 0, "E"), # 6W - }, - 2: { - "N": lambda y, x: (4 * SIZE - 1, x - 2 * SIZE, "N"), # 6S - "E": lambda y, x: (3 * SIZE - y - 1, 2 * SIZE - 1, "W"), # 5E - "S": lambda y, x: (x - SIZE, 2 * SIZE - 1, "W"), # 3E - }, - 3: { - "W": lambda y, x: (2 * SIZE, y - SIZE, "S"), # 4N - "E": lambda y, x: (SIZE - 1, SIZE + y, "N"), # 2S - }, - 4: { - "W": lambda y, x: (3 * SIZE - y - 1, SIZE, "E"), # 1W - "N": lambda y, x: (SIZE + x, SIZE, "E"), # 3W - }, - 5: { - "E": lambda y, x: (3 * SIZE - y - 1, 3 * SIZE - 1, "W"), # 2E - "S": lambda y, x: (2 * SIZE + x, SIZE - 1, "W"), # 6E - }, - 6: { - "W": lambda y, x: (0, y - 2 * SIZE, "S"), # 1N - "E": lambda y, x: (3 * SIZE - 1, y - 2 * SIZE, "N"), # 5S - "S": lambda y, x: (0, x + 2 * SIZE, "S"), # 2N - }, - } - - -def wrap_part_1(y0: int, x0: int, r0: str) -> tuple[int, int, str]: - if r0 == "E": - return y0, row_first_non_void[y0], r0 - elif r0 == "S": - return col_first_non_void[x0], x0, r0 - elif r0 == "W": - return y0, row_last_non_void[y0], r0 - elif r0 == "N": - return col_last_non_void[x0], x0, r0 - - assert False - - -def wrap_part_2(y0: int, x0: int, r0: str) -> tuple[int, int, str]: - cube = faces[y0, x0] - assert r0 in faces_wrap[cube] - return faces_wrap[cube][r0](y0, x0) - - -def run(wrap: Callable[[int, int, str], tuple[int, int, str]]) -> tuple[int, int, str]: - y0 = 0 - x0 = np.where(board[0] == EMPTY)[0][0] - r0 = "E" - - for direction in directions: - if isinstance(direction, int): - while direction > 0: - if r0 == "E": - xi = np.where(board[y0, x0 + 1 : x0 + direction + 1] == WALL)[0] - if len(xi): - x0 = x0 + xi[0] - direction = 0 - elif ( - x0 + direction < board.shape[1] - and board[y0, x0 + direction] == EMPTY - ): - x0 = x0 + direction - direction = 0 - else: - y0_t, x0_t, r0_t = wrap(y0, x0, r0) - if board[y0_t, x0_t] == WALL: - x0 = row_last_non_void[y0] - direction = 0 - else: - direction = direction - (row_last_non_void[y0] - x0) - 1 - y0, x0, r0 = y0_t, x0_t, r0_t - elif r0 == "S": - yi = np.where(board[y0 + 1 : y0 + direction + 1, x0] == WALL)[0] - if len(yi): - y0 = y0 + yi[0] - direction = 0 - elif ( - y0 + direction < board.shape[0] - and board[y0 + direction, x0] == EMPTY - ): - y0 = y0 + direction - direction = 0 - else: - y0_t, x0_t, r0_t = wrap(y0, x0, r0) - if board[y0_t, x0_t] == WALL: - y0 = col_last_non_void[x0] - direction = 0 - else: - direction = direction - (col_last_non_void[x0] - y0) - 1 - y0, x0, r0 = y0_t, x0_t, r0_t - elif r0 == "W": - left = max(x0 - direction - 1, 0) - xi = np.where(board[y0, left:x0] == WALL)[0] - if len(xi): - x0 = left + xi[-1] + 1 - direction = 0 - elif x0 - direction >= 0 and board[y0, x0 - direction] == EMPTY: - x0 = x0 - direction - direction = 0 - else: - y0_t, x0_t, r0_t = wrap(y0, x0, r0) - if board[y0_t, x0_t] == WALL: - x0 = row_first_non_void[y0] - direction = 0 - else: - direction = direction - (x0 - row_first_non_void[y0]) - 1 - y0, x0, r0 = y0_t, x0_t, r0_t - elif r0 == "N": - top = max(y0 - direction - 1, 0) - yi = np.where(board[top:y0, x0] == WALL)[0] - if len(yi): - y0 = top + yi[-1] + 1 - direction = 0 - elif y0 - direction >= 0 and board[y0 - direction, x0] == EMPTY: - y0 = y0 - direction - direction = 0 - else: - y0_t, x0_t, r0_t = wrap(y0, x0, r0) - if board[y0_t, x0_t] == WALL: - y0 = col_first_non_void[x0] - direction = 0 - else: - direction = direction - (y0 - col_first_non_void[x0]) - 1 - y0, x0, r0 = y0_t, x0_t, r0_t else: - r0 = { - "E": {"L": "N", "R": "S"}, - "N": {"L": "W", "R": "E"}, - "W": {"L": "S", "R": "N"}, - "S": {"L": "E", "R": "W"}, - }[r0][direction] + faces_wrap = { + 1: { + "W": lambda y, x: (3 * SIZE - y - 1, 0, "E"), # 4W + "N": lambda y, x: (2 * SIZE + x, 0, "E"), # 6W + }, + 2: { + "N": lambda y, x: (4 * SIZE - 1, x - 2 * SIZE, "N"), # 6S + "E": lambda y, x: (3 * SIZE - y - 1, 2 * SIZE - 1, "W"), # 5E + "S": lambda y, x: (x - SIZE, 2 * SIZE - 1, "W"), # 3E + }, + 3: { + "W": lambda y, x: (2 * SIZE, y - SIZE, "S"), # 4N + "E": lambda y, x: (SIZE - 1, SIZE + y, "N"), # 2S + }, + 4: { + "W": lambda y, x: (3 * SIZE - y - 1, SIZE, "E"), # 1W + "N": lambda y, x: (SIZE + x, SIZE, "E"), # 3W + }, + 5: { + "E": lambda y, x: (3 * SIZE - y - 1, 3 * SIZE - 1, "W"), # 2E + "S": lambda y, x: (2 * SIZE + x, SIZE - 1, "W"), # 6E + }, + 6: { + "W": lambda y, x: (0, y - 2 * SIZE, "S"), # 1N + "E": lambda y, x: (3 * SIZE - 1, y - 2 * SIZE, "N"), # 5S + "S": lambda y, x: (0, x + 2 * SIZE, "S"), # 2N + }, + } - return y0, x0, r0 + def wrap_part_1(y0: int, x0: int, r0: str) -> tuple[int, int, str]: + if r0 == "E": + return y0, row_first_non_void[y0], r0 + elif r0 == "S": + return col_first_non_void[x0], x0, r0 + elif r0 == "W": + return y0, row_last_non_void[y0], r0 + elif r0 == "N": + return col_last_non_void[x0], x0, r0 + assert False -y1, x1, r1 = run(wrap_part_1) -answer_1 = 1000 * (1 + y1) + 4 * (1 + x1) + SCORES[r1] -print(f"answer 1 is {answer_1}") + def wrap_part_2(y0: int, x0: int, r0: str) -> tuple[int, int, str]: + cube = faces[y0, x0] + assert r0 in faces_wrap[cube] + return faces_wrap[cube][r0](y0, x0) -y2, x2, r2 = run(wrap_part_2) -answer_2 = 1000 * (1 + y2) + 4 * (1 + x2) + SCORES[r2] -print(f"answer 2 is {answer_2}") + def run( + wrap: Callable[[int, int, str], tuple[int, int, str]], + ) -> tuple[int, int, str]: + y0 = 0 + x0 = np.where(board[0] == EMPTY)[0][0] + r0 = "E" + + for direction in directions: + if isinstance(direction, int): + while direction > 0: + if r0 == "E": + xi = np.where( + board[y0, x0 + 1 : x0 + direction + 1] == WALL + )[0] + if len(xi): + x0 = x0 + xi[0] + direction = 0 + elif ( + x0 + direction < board.shape[1] + and board[y0, x0 + direction] == EMPTY + ): + x0 = x0 + direction + direction = 0 + else: + y0_t, x0_t, r0_t = wrap(y0, x0, r0) + if board[y0_t, x0_t] == WALL: + x0 = row_last_non_void[y0] + direction = 0 + else: + direction = ( + direction - (row_last_non_void[y0] - x0) - 1 + ) + y0, x0, r0 = y0_t, x0_t, r0_t + elif r0 == "S": + yi = np.where( + board[y0 + 1 : y0 + direction + 1, x0] == WALL + )[0] + if len(yi): + y0 = y0 + yi[0] + direction = 0 + elif ( + y0 + direction < board.shape[0] + and board[y0 + direction, x0] == EMPTY + ): + y0 = y0 + direction + direction = 0 + else: + y0_t, x0_t, r0_t = wrap(y0, x0, r0) + if board[y0_t, x0_t] == WALL: + y0 = col_last_non_void[x0] + direction = 0 + else: + direction = ( + direction - (col_last_non_void[x0] - y0) - 1 + ) + y0, x0, r0 = y0_t, x0_t, r0_t + elif r0 == "W": + left = max(x0 - direction - 1, 0) + xi = np.where(board[y0, left:x0] == WALL)[0] + if len(xi): + x0 = left + xi[-1] + 1 + direction = 0 + elif ( + x0 - direction >= 0 + and board[y0, x0 - direction] == EMPTY + ): + x0 = x0 - direction + direction = 0 + else: + y0_t, x0_t, r0_t = wrap(y0, x0, r0) + if board[y0_t, x0_t] == WALL: + x0 = row_first_non_void[y0] + direction = 0 + else: + direction = ( + direction - (x0 - row_first_non_void[y0]) - 1 + ) + y0, x0, r0 = y0_t, x0_t, r0_t + elif r0 == "N": + top = max(y0 - direction - 1, 0) + yi = np.where(board[top:y0, x0] == WALL)[0] + if len(yi): + y0 = top + yi[-1] + 1 + direction = 0 + elif ( + y0 - direction >= 0 + and board[y0 - direction, x0] == EMPTY + ): + y0 = y0 - direction + direction = 0 + else: + y0_t, x0_t, r0_t = wrap(y0, x0, r0) + if board[y0_t, x0_t] == WALL: + y0 = col_first_non_void[x0] + direction = 0 + else: + direction = ( + direction - (y0 - col_first_non_void[x0]) - 1 + ) + y0, x0, r0 = y0_t, x0_t, r0_t + else: + r0 = { + "E": {"L": "N", "R": "S"}, + "N": {"L": "W", "R": "E"}, + "W": {"L": "S", "R": "N"}, + "S": {"L": "E", "R": "W"}, + }[r0][direction] + + return y0, x0, r0 + + y1, x1, r1 = run(wrap_part_1) + yield 1000 * (1 + y1) + 4 * (1 + x1) + SCORES[r1] + + y2, x2, r2 = run(wrap_part_2) + yield 1000 * (1 + y2) + 4 * (1 + x2) + SCORES[r2] diff --git a/src/holt59/aoc/2022/day23.py b/src/holt59/aoc/2022/day23.py index 902c20b..0695dd9 100644 --- a/src/holt59/aoc/2022/day23.py +++ b/src/holt59/aoc/2022/day23.py @@ -1,6 +1,8 @@ import itertools -import sys from collections import defaultdict +from typing import Any, Iterator + +from ..base import BaseSolver Directions = list[ tuple[ @@ -18,7 +20,7 @@ DIRECTIONS: Directions = [ def min_max_yx(positions: set[tuple[int, int]]) -> tuple[int, int, int, int]: - ys, xs = {y for y, x in positions}, {x for y, x in positions} + ys, xs = {y for y, _x in positions}, {x for _y, x in positions} return min(ys), min(xs), max(ys), max(xs) @@ -69,35 +71,38 @@ def round( directions.append(directions.pop(0)) -POSITIONS = { - (i, j) - for i, row in enumerate(sys.stdin.read().splitlines()) - for j, col in enumerate(row) - if col == "#" -} +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + POSITIONS = { + (i, j) + for i, row in enumerate(input.splitlines()) + for j, col in enumerate(row) + if col == "#" + } -# === part 1 === + # === part 1 === -p1, d1 = POSITIONS.copy(), DIRECTIONS.copy() -for r in range(10): - round(p1, d1) + p1, d1 = POSITIONS.copy(), DIRECTIONS.copy() + for _ in range(10): + round(p1, d1) -min_y, min_x, max_y, max_x = min_max_yx(p1) -answer_1 = sum( - (y, x) not in p1 for y in range(min_y, max_y + 1) for x in range(min_x, max_x + 1) -) -print(f"answer 1 is {answer_1}") + min_y, min_x, max_y, max_x = min_max_yx(p1) + yield sum( + (y, x) not in p1 + for y in range(min_y, max_y + 1) + for x in range(min_x, max_x + 1) + ) -# === part 2 === + # === part 2 === -p2, d2 = POSITIONS.copy(), DIRECTIONS.copy() -answer_2 = 0 -while True: - answer_2 += 1 - backup = p2.copy() - round(p2, d2) + p2, d2 = POSITIONS.copy(), DIRECTIONS.copy() + answer_2 = 0 + while True: + answer_2 += 1 + backup = p2.copy() + round(p2, d2) - if backup == p2: - break + if backup == p2: + break -print(f"answer 2 is {answer_2}") + yield answer_2 diff --git a/src/holt59/aoc/2022/day24.py b/src/holt59/aoc/2022/day24.py index 6ea76b9..f7f5cc4 100644 --- a/src/holt59/aoc/2022/day24.py +++ b/src/holt59/aoc/2022/day24.py @@ -1,98 +1,117 @@ import heapq import math -import sys from collections import defaultdict +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() - -winds = { - (i - 1, j - 1, lines[i][j]) - for i in range(1, len(lines) - 1) - for j in range(1, len(lines[i]) - 1) - if lines[i][j] != "." -} - -n_rows, n_cols = len(lines) - 2, len(lines[0]) - 2 -CYCLE = math.lcm(n_rows, n_cols) - -east_winds = [{j for j in range(n_cols) if (i, j, ">") in winds} for i in range(n_rows)] -west_winds = [{j for j in range(n_cols) if (i, j, "<") in winds} for i in range(n_rows)] -north_winds = [ - {i for i in range(n_rows) if (i, j, "^") in winds} for j in range(n_cols) -] -south_winds = [ - {i for i in range(n_rows) if (i, j, "v") in winds} for j in range(n_cols) -] +from ..base import BaseSolver -def run(start: tuple[int, int], start_cycle: int, end: tuple[int, int]): - def heuristic(y: int, x: int) -> int: - return abs(end[0] - y) + abs(end[1] - x) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] - # (distance + heuristic, distance, (start_pos, cycle)) - queue = [(heuristic(start[0], start[1]), 0, ((start[0], start[1]), start_cycle))] - visited: set[tuple[tuple[int, int], int]] = set() - distances: dict[tuple[int, int], dict[int, int]] = defaultdict(lambda: {}) + winds = { + (i - 1, j - 1, lines[i][j]) + for i in range(1, len(lines) - 1) + for j in range(1, len(lines[i]) - 1) + if lines[i][j] != "." + } - while queue: - _, distance, ((y, x), cycle) = heapq.heappop(queue) + n_rows, n_cols = len(lines) - 2, len(lines[0]) - 2 + CYCLE = math.lcm(n_rows, n_cols) - if ((y, x), cycle) in visited: - continue + east_winds = [ + {j for j in range(n_cols) if (i, j, ">") in winds} for i in range(n_rows) + ] + west_winds = [ + {j for j in range(n_cols) if (i, j, "<") in winds} for i in range(n_rows) + ] + north_winds = [ + {i for i in range(n_rows) if (i, j, "^") in winds} for j in range(n_cols) + ] + south_winds = [ + {i for i in range(n_rows) if (i, j, "v") in winds} for j in range(n_cols) + ] - distances[y, x][cycle] = distance + def run(start: tuple[int, int], start_cycle: int, end: tuple[int, int]): + def heuristic(y: int, x: int) -> int: + return abs(end[0] - y) + abs(end[1] - x) - visited.add(((y, x), cycle)) + # (distance + heuristic, distance, (start_pos, cycle)) + queue = [ + (heuristic(start[0], start[1]), 0, ((start[0], start[1]), start_cycle)) + ] + visited: set[tuple[tuple[int, int], int]] = set() + distances: dict[tuple[int, int], dict[int, int]] = defaultdict(lambda: {}) - if (y, x) == (end[0], end[1]): - break + while queue: + _, distance, ((y, x), cycle) = heapq.heappop(queue) - for dy, dx in (0, 0), (-1, 0), (1, 0), (0, -1), (0, 1): - ty = y + dy - tx = x + dx - - n_cycle = (cycle + 1) % CYCLE - - if (ty, tx) == end: - heapq.heappush(queue, (distance + 1, distance + 1, ((ty, tx), n_cycle))) - break - - if ((ty, tx), n_cycle) in visited: - continue - - if (ty, tx) != start and (ty < 0 or tx < 0 or ty >= n_rows or tx >= n_cols): - continue - - if (ty, tx) != start: - if (ty - n_cycle) % n_rows in south_winds[tx]: - continue - if (ty + n_cycle) % n_rows in north_winds[tx]: - continue - if (tx + n_cycle) % n_cols in west_winds[ty]: - continue - if (tx - n_cycle) % n_cols in east_winds[ty]: + if ((y, x), cycle) in visited: continue - heapq.heappush( - queue, - ((heuristic(ty, tx) + distance + 1, distance + 1, ((ty, tx), n_cycle))), - ) + distances[y, x][cycle] = distance - return distances, next(iter(distances[end].values())) + visited.add(((y, x), cycle)) + if (y, x) == (end[0], end[1]): + break -start = ( - -1, - next(j for j in range(1, len(lines[0]) - 1) if lines[0][j] == ".") - 1, -) -end = ( - n_rows, - next(j for j in range(1, len(lines[-1]) - 1) if lines[-1][j] == ".") - 1, -) + for dy, dx in (0, 0), (-1, 0), (1, 0), (0, -1), (0, 1): + ty = y + dy + tx = x + dx -distances_1, forward_1 = run(start, 0, end) -print(f"answer 1 is {forward_1}") + n_cycle = (cycle + 1) % CYCLE -distances_2, return_1 = run(end, next(iter(distances_1[end].keys())), start) -distances_3, forward_2 = run(start, next(iter(distances_2[start].keys())), end) -print(f"answer 2 is {forward_1 + return_1 + forward_2}") + if (ty, tx) == end: + heapq.heappush( + queue, (distance + 1, distance + 1, ((ty, tx), n_cycle)) + ) + break + + if ((ty, tx), n_cycle) in visited: + continue + + if (ty, tx) != start and ( + ty < 0 or tx < 0 or ty >= n_rows or tx >= n_cols + ): + continue + + if (ty, tx) != start: + if (ty - n_cycle) % n_rows in south_winds[tx]: + continue + if (ty + n_cycle) % n_rows in north_winds[tx]: + continue + if (tx + n_cycle) % n_cols in west_winds[ty]: + continue + if (tx - n_cycle) % n_cols in east_winds[ty]: + continue + + heapq.heappush( + queue, + ( + ( + heuristic(ty, tx) + distance + 1, + distance + 1, + ((ty, tx), n_cycle), + ) + ), + ) + + return distances, next(iter(distances[end].values())) + + start = ( + -1, + next(j for j in range(1, len(lines[0]) - 1) if lines[0][j] == ".") - 1, + ) + end = ( + n_rows, + next(j for j in range(1, len(lines[-1]) - 1) if lines[-1][j] == ".") - 1, + ) + + distances_1, forward_1 = run(start, 0, end) + yield forward_1 + + distances_2, return_1 = run(end, next(iter(distances_1[end].keys())), start) + _distances_3, forward_2 = run(start, next(iter(distances_2[start].keys())), end) + yield forward_1 + return_1 + forward_2 diff --git a/src/holt59/aoc/2022/day25.py b/src/holt59/aoc/2022/day25.py index 4327247..778be4b 100644 --- a/src/holt59/aoc/2022/day25.py +++ b/src/holt59/aoc/2022/day25.py @@ -1,27 +1,28 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() - -coeffs = {"2": 2, "1": 1, "0": 0, "-": -1, "=": -2} +from ..base import BaseSolver -def snafu2number(number: str) -> int: - value = 0 - for c in number: - value *= 5 - value += coeffs[c] - return value +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] + coeffs = {"2": 2, "1": 1, "0": 0, "-": -1, "=": -2} -def number2snafu(number: int) -> str: - values = ["0", "1", "2", "=", "-"] - res = "" - while number > 0: - mod = number % 5 - res = res + values[mod] - number = number // 5 + int(mod >= 3) - return "".join(reversed(res)) + def snafu2number(number: str) -> int: + value = 0 + for c in number: + value *= 5 + value += coeffs[c] + return value + def number2snafu(number: int) -> str: + values = ["0", "1", "2", "=", "-"] + res = "" + while number > 0: + mod = number % 5 + res = res + values[mod] + number = number // 5 + int(mod >= 3) + return "".join(reversed(res)) -answer_1 = number2snafu(sum(map(snafu2number, lines))) -print(f"answer 1 is {answer_1}") + yield number2snafu(sum(map(snafu2number, lines))) diff --git a/src/holt59/aoc/2022/day3.py b/src/holt59/aoc/2022/day3.py index b7740ed..26f0eef 100644 --- a/src/holt59/aoc/2022/day3.py +++ b/src/holt59/aoc/2022/day3.py @@ -1,23 +1,28 @@ import string -import sys +from typing import Any, Iterator -lines = [line.strip() for line in sys.stdin.readlines()] +from ..base import BaseSolver -# extract content of each part -parts = [(set(line[: len(line) // 2]), set(line[len(line) // 2 :])) for line in lines] -# priorities -priorities = {c: i + 1 for i, c in enumerate(string.ascii_letters)} +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] -# part 1 -part1 = sum(priorities[c] for p1, p2 in parts for c in p1.intersection(p2)) -print(f"answer 1 is {part1}") + # extract content of each part + parts = [ + (set(line[: len(line) // 2]), set(line[len(line) // 2 :])) for line in lines + ] -# part 2 -n_per_group = 3 -part2 = sum( - priorities[c] - for i in range(0, len(lines), n_per_group) - for c in set(lines[i]).intersection(*lines[i + 1 : i + n_per_group]) -) -print(f"answer 2 is {part2}") + # priorities + priorities = {c: i + 1 for i, c in enumerate(string.ascii_letters)} + + # part 1 + yield sum(priorities[c] for p1, p2 in parts for c in p1.intersection(p2)) + + # part 2 + n_per_group = 3 + yield sum( + priorities[c] + for i in range(0, len(lines), n_per_group) + for c in set(lines[i]).intersection(*lines[i + 1 : i + n_per_group]) + ) diff --git a/src/holt59/aoc/2022/day4.py b/src/holt59/aoc/2022/day4.py index bf6aae1..dfbb713 100644 --- a/src/holt59/aoc/2022/day4.py +++ b/src/holt59/aoc/2022/day4.py @@ -1,6 +1,6 @@ -import sys +from typing import Any, Iterator -lines = [line.strip() for line in sys.stdin.readlines()] +from ..base import BaseSolver def make_range(value: str) -> set[int]: @@ -8,10 +8,13 @@ def make_range(value: str) -> set[int]: return set(range(int(parts[0]), int(parts[1]) + 1)) -sections = [tuple(make_range(part) for part in line.split(",")) for line in lines] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] -answer_1 = sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections) -print(f"answer 1 is {answer_1}") + sections = [ + tuple(make_range(part) for part in line.split(",")) for line in lines + ] -answer_2 = sum(bool(s1.intersection(s2)) for s1, s2 in sections) -print(f"answer 1 is {answer_2}") + yield sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections) + yield sum(bool(s1.intersection(s2)) for s1, s2 in sections) diff --git a/src/holt59/aoc/2022/day5.py b/src/holt59/aoc/2022/day5.py index 04ba94f..831470c 100644 --- a/src/holt59/aoc/2022/day5.py +++ b/src/holt59/aoc/2022/day5.py @@ -1,41 +1,43 @@ import copy -import sys +from typing import Any, Iterator -blocks_s, moves_s = (part.splitlines() for part in sys.stdin.read().split("\n\n")) +from ..base import BaseSolver -blocks: dict[str, list[str]] = {stack: [] for stack in blocks_s[-1].split()} -# this codes assumes that the lines are regular, i.e., 4 characters per "crate" in the -# form of '[X] ' (including the trailing space) -# -for block in blocks_s[-2::-1]: - for stack, index in zip(blocks, range(0, len(block), 4)): - crate = block[index + 1 : index + 2].strip() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + blocks_s, moves_s = (part.splitlines() for part in input.split("\n\n")) - if crate: - blocks[stack].append(crate) + blocks: dict[str, list[str]] = {stack: [] for stack in blocks_s[-1].split()} -# part 1 - deep copy for part 2 -blocks_1 = copy.deepcopy(blocks) + # this codes assumes that the lines are regular, i.e., 4 characters per "crate" in the + # form of '[X] ' (including the trailing space) + # + for block in blocks_s[-2::-1]: + for stack, index in zip(blocks, range(0, len(block), 4)): + crate = block[index + 1 : index + 2].strip() -for move in moves_s: - _, count_s, _, from_, _, to_ = move.strip().split() + if crate: + blocks[stack].append(crate) - for _i in range(int(count_s)): - blocks_1[to_].append(blocks_1[from_].pop()) + # part 1 - deep copy for part 2 + blocks_1 = copy.deepcopy(blocks) -# part 2 -blocks_2 = copy.deepcopy(blocks) + for move in moves_s: + _, count_s, _, from_, _, to_ = move.strip().split() -for move in moves_s: - _, count_s, _, from_, _, to_ = move.strip().split() - count = int(count_s) + for _i in range(int(count_s)): + blocks_1[to_].append(blocks_1[from_].pop()) - blocks_2[to_].extend(blocks_2[from_][-count:]) - del blocks_2[from_][-count:] + # part 2 + blocks_2 = copy.deepcopy(blocks) -answer_1 = "".join(s[-1] for s in blocks_1.values()) -print(f"answer 1 is {answer_1}") + for move in moves_s: + _, count_s, _, from_, _, to_ = move.strip().split() + count = int(count_s) -answer_2 = "".join(s[-1] for s in blocks_2.values()) -print(f"answer 2 is {answer_2}") + blocks_2[to_].extend(blocks_2[from_][-count:]) + del blocks_2[from_][-count:] + + yield "".join(s[-1] for s in blocks_1.values()) + yield "".join(s[-1] for s in blocks_2.values()) diff --git a/src/holt59/aoc/2022/day6.py b/src/holt59/aoc/2022/day6.py index 086f33e..db7faf6 100644 --- a/src/holt59/aoc/2022/day6.py +++ b/src/holt59/aoc/2022/day6.py @@ -1,4 +1,6 @@ -import sys +from typing import Any, Iterator + +from ..base import BaseSolver def index_of_first_n_differents(data: str, n: int) -> int: @@ -8,8 +10,7 @@ def index_of_first_n_differents(data: str, n: int) -> int: return -1 -data = sys.stdin.read().strip() - - -print(f"answer 1 is {index_of_first_n_differents(data, 4)}") -print(f"answer 2 is {index_of_first_n_differents(data, 14)}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + yield index_of_first_n_differents(input, 4) + yield index_of_first_n_differents(input, 14) diff --git a/src/holt59/aoc/2022/day7.py b/src/holt59/aoc/2022/day7.py index c95e1b9..a3cb585 100644 --- a/src/holt59/aoc/2022/day7.py +++ b/src/holt59/aoc/2022/day7.py @@ -1,80 +1,81 @@ -import sys from pathlib import Path +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() - -# we are going to use Path to create path and go up/down in the file tree since it -# implements everything we need -# -# we can use .resolve() to get normalized path, although this will add C:\ to all paths -# on Windows but that is not an issue since only the sizes matter -# - -# mapping from path to list of files or directories -trees: dict[Path, list[Path]] = {} - -# mapping from paths to either size (for file) or -1 for directory -sizes: dict[Path, int] = {} - -# first line must be a cd otherwise we have no idea where we are -assert lines[0].startswith("$ cd") -base_path = Path(lines[0].strip("$").split()[1]).resolve() -cur_path = base_path - -trees[cur_path] = [] -sizes[cur_path] = -1 - -for line in lines[1:]: - # command - if line.startswith("$"): - parts = line.strip("$").strip().split() - command = parts[0] - - if command == "cd": - cur_path = cur_path.joinpath(parts[1]).resolve() - - # just initialize the lis of files if not already done - if cur_path not in trees: - trees[cur_path] = [] - else: - # nothing to do here - pass - - # fill the current path - else: - parts = line.split() - name: str = parts[1] - if line.startswith("dir"): - size = -1 - else: - size = int(parts[0]) - - path = cur_path.joinpath(name) - trees[cur_path].append(path) - sizes[path] = size +from ..base import BaseSolver -def compute_size(path: Path) -> int: - size = sizes[path] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] - if size >= 0: - return size + # we are going to use Path to create path and go up/down in the file tree since it + # implements everything we need + # + # we can use .resolve() to get normalized path, although this will add C:\ to all paths + # on Windows but that is not an issue since only the sizes matter + # - return sum(compute_size(sub) for sub in trees[path]) + # mapping from path to list of files or directories + trees: dict[Path, list[Path]] = {} + # mapping from paths to either size (for file) or -1 for directory + sizes: dict[Path, int] = {} -acc_sizes = {path: compute_size(path) for path in trees} + # first line must be a cd otherwise we have no idea where we are + assert lines[0].startswith("$ cd") + base_path = Path(lines[0].strip("$").split()[1]).resolve() + cur_path = base_path -# part 1 -answer_1 = sum(size for size in acc_sizes.values() if size <= 100_000) -print(f"answer 1 is {answer_1}") + trees[cur_path] = [] + sizes[cur_path] = -1 -# part 2 -total_space = 70_000_000 -update_space = 30_000_000 -free_space = total_space - acc_sizes[base_path] + for line in lines[1:]: + # command + if line.startswith("$"): + parts = line.strip("$").strip().split() + command = parts[0] -to_free_space = update_space - free_space + if command == "cd": + cur_path = cur_path.joinpath(parts[1]).resolve() -answer_2 = min(size for size in acc_sizes.values() if size >= to_free_space) -print(f"answer 2 is {answer_2}") + # just initialize the lis of files if not already done + if cur_path not in trees: + trees[cur_path] = [] + else: + # nothing to do here + pass + + # fill the current path + else: + parts = line.split() + name: str = parts[1] + if line.startswith("dir"): + size = -1 + else: + size = int(parts[0]) + + path = cur_path.joinpath(name) + trees[cur_path].append(path) + sizes[path] = size + + def compute_size(path: Path) -> int: + size = sizes[path] + + if size >= 0: + return size + + return sum(compute_size(sub) for sub in trees[path]) + + acc_sizes = {path: compute_size(path) for path in trees} + + # part 1 + yield sum(size for size in acc_sizes.values() if size <= 100_000) + + # part 2 + total_space = 70_000_000 + update_space = 30_000_000 + free_space = total_space - acc_sizes[base_path] + + to_free_space = update_space - free_space + + yield min(size for size in acc_sizes.values() if size >= to_free_space) diff --git a/src/holt59/aoc/2022/day8.py b/src/holt59/aoc/2022/day8.py index ee75ede..ba06574 100644 --- a/src/holt59/aoc/2022/day8.py +++ b/src/holt59/aoc/2022/day8.py @@ -1,53 +1,54 @@ -import sys +from typing import Any, Iterator import numpy as np from numpy.typing import NDArray -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -trees = np.array([[int(x) for x in row] for row in lines]) -# answer 1 -highest_trees = np.ones(trees.shape + (4,), dtype=int) * -1 -highest_trees[1:-1, 1:-1] = [ - [ - [ - trees[:i, j].max(), - trees[i + 1 :, j].max(), - trees[i, :j].max(), - trees[i, j + 1 :].max(), +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] + + trees = np.array([[int(x) for x in row] for row in lines]) + + # answer 1 + highest_trees = np.ones(trees.shape + (4,), dtype=int) * -1 + highest_trees[1:-1, 1:-1] = [ + [ + [ + trees[:i, j].max(), + trees[i + 1 :, j].max(), + trees[i, :j].max(), + trees[i, j + 1 :].max(), + ] + for j in range(1, trees.shape[1] - 1) + ] + for i in range(1, trees.shape[0] - 1) ] - for j in range(1, trees.shape[1] - 1) - ] - for i in range(1, trees.shape[0] - 1) -] -answer_1 = (highest_trees.min(axis=2) < trees).sum() -print(f"answer 1 is {answer_1}") + yield (highest_trees.min(axis=2) < trees).sum() + def viewing_distance(row_of_trees: NDArray[np.int_], value: int) -> int: + w = np.where(row_of_trees >= value)[0] -def viewing_distance(row_of_trees: NDArray[np.int_], value: int) -> int: - w = np.where(row_of_trees >= value)[0] + if not w.size: + return len(row_of_trees) - if not w.size: - return len(row_of_trees) + return w[0] + 1 - return w[0] + 1 - - -# answer 2 -v_distances = np.zeros(trees.shape + (4,), dtype=int) -v_distances[1:-1, 1:-1, :] = [ - [ - [ - viewing_distance(trees[i - 1 :: -1, j], trees[i, j]), - viewing_distance(trees[i, j - 1 :: -1], trees[i, j]), - viewing_distance(trees[i, j + 1 :], trees[i, j]), - viewing_distance(trees[i + 1 :, j], trees[i, j]), + # answer 2 + v_distances = np.zeros(trees.shape + (4,), dtype=int) + v_distances[1:-1, 1:-1, :] = [ + [ + [ + viewing_distance(trees[i - 1 :: -1, j], trees[i, j]), + viewing_distance(trees[i, j - 1 :: -1], trees[i, j]), + viewing_distance(trees[i, j + 1 :], trees[i, j]), + viewing_distance(trees[i + 1 :, j], trees[i, j]), + ] + for j in range(1, trees.shape[1] - 1) + ] + for i in range(1, trees.shape[0] - 1) ] - for j in range(1, trees.shape[1] - 1) - ] - for i in range(1, trees.shape[0] - 1) -] -answer_2 = np.prod(v_distances, axis=2).max() -print(f"answer 2 is {answer_2}") + yield np.prod(v_distances, axis=2).max() diff --git a/src/holt59/aoc/2022/day9.py b/src/holt59/aoc/2022/day9.py index f81ec93..6bfa243 100644 --- a/src/holt59/aoc/2022/day9.py +++ b/src/holt59/aoc/2022/day9.py @@ -1,7 +1,10 @@ -import sys +import itertools as it +from typing import Any, Iterator import numpy as np +from ..base import BaseSolver + def move(head: tuple[int, int], command: str) -> tuple[int, int]: h_col, h_row = head @@ -43,17 +46,14 @@ def run(commands: list[str], n_blocks: int) -> list[tuple[int, int]]: return visited -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] -# flatten the commands -commands: list[str] = [] -for line in lines: - d, c = line.split() - commands.extend(d * int(c)) + # flatten the commands + commands = list( + it.chain(*(p[0] * int(p[1]) for line in lines if (p := line.split()))) + ) - -visited_1 = run(commands, n_blocks=2) -print(f"answer 1 is {len(set(visited_1))}") - -visited_2 = run(commands, n_blocks=10) -print(f"answer 2 is {len(set(visited_2))}") + yield len(set(run(commands, n_blocks=2))) + yield len(set(run(commands, n_blocks=10))) diff --git a/src/holt59/aoc/2023/day1.py b/src/holt59/aoc/2023/day1.py index 3acd9ad..9fe88f7 100644 --- a/src/holt59/aoc/2023/day1.py +++ b/src/holt59/aoc/2023/day1.py @@ -1,27 +1,9 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() - -lookups_1 = {str(d): d for d in range(1, 10)} -lookups_2 = lookups_1 | { - d: i + 1 - for i, d in enumerate( - ( - "one", - "two", - "three", - "four", - "five", - "six", - "seven", - "eight", - "nine", - ) - ) -} +from ..base import BaseSolver -def find_values(lookups: dict[str, int]) -> list[int]: +def find_values(lines: list[str], lookups: dict[str, int]) -> list[int]: values: list[int] = [] for line in filter(bool, lines): @@ -41,5 +23,27 @@ def find_values(lookups: dict[str, int]) -> list[int]: return values -print(f"answer 1 is {sum(find_values(lookups_1))}") -print(f"answer 2 is {sum(find_values(lookups_2))}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lookups_1 = {str(d): d for d in range(1, 10)} + lookups_2 = lookups_1 | { + d: i + 1 + for i, d in enumerate( + ( + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + ) + ) + } + + lines = input.splitlines() + + yield sum(find_values(lines, lookups_1)) + yield sum(find_values(lines, lookups_2)) diff --git a/src/holt59/aoc/2023/day10.py b/src/holt59/aoc/2023/day10.py index 473dac5..eee3f1d 100644 --- a/src/holt59/aoc/2023/day10.py +++ b/src/holt59/aoc/2023/day10.py @@ -1,100 +1,100 @@ -import os -import sys -from typing import Literal, cast +from typing import Any, Iterator, Literal, cast -VERBOSE = os.getenv("AOC_VERBOSE") == "True" +from ..base import BaseSolver Symbol = Literal["|", "-", "L", "J", "7", "F", ".", "S"] -lines: list[list[Symbol]] = [ - [cast(Symbol, symbol) for symbol in line] for line in sys.stdin.read().splitlines() -] -# find starting point -si, sj = next( - (i, j) - for i in range(len(lines)) - for j in range(len(lines[0])) - if lines[i][j] == "S" -) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines: list[list[Symbol]] = [ + [cast(Symbol, symbol) for symbol in line] for line in input.splitlines() + ] -# find one of the two outputs -ni, nj = si, sj -for ni, nj, chars in ( - (si - 1, sj, "|7F"), - (si + 1, sj, "|LJ"), - (si, sj - 1, "-LF"), - (si, sj + 1, "-J7"), -): - if lines[ni][nj] in chars: - break + # find starting point + si, sj = next( + (i, j) + for i in range(len(lines)) + for j in range(len(lines[0])) + if lines[i][j] == "S" + ) -# part 1 - find the loop (re-used in part 2) -loop = [(si, sj), (ni, nj)] -while True: - pi, pj = loop[-2] - i, j = loop[-1] + # find one of the two outputs + ni, nj = si, sj + for ni, nj, chars in ( + (si - 1, sj, "|7F"), + (si + 1, sj, "|LJ"), + (si, sj - 1, "-LF"), + (si, sj + 1, "-J7"), + ): + if lines[ni][nj] in chars: + break - sym = lines[i][j] + # part 1 - find the loop (re-used in part 2) + loop = [(si, sj), (ni, nj)] + while True: + pi, pj = loop[-2] + i, j = loop[-1] - if sym == "|" and pi > i or sym in "JL" and pi == i: - i -= 1 - elif sym == "|" and pi < i or sym in "7F" and pi == i: - i += 1 - elif sym == "-" and pj > j or sym in "J7" and pj == j: - j -= 1 - elif sym == "-" and pj < j or sym in "LF" and pj == j: - j += 1 + sym = lines[i][j] - if (i, j) == (si, sj): - break + if sym == "|" and pi > i or sym in "JL" and pi == i: + i -= 1 + elif sym == "|" and pi < i or sym in "7F" and pi == i: + i += 1 + elif sym == "-" and pj > j or sym in "J7" and pj == j: + j -= 1 + elif sym == "-" and pj < j or sym in "LF" and pj == j: + j += 1 - loop.append((i, j)) - -answer_1 = len(loop) // 2 -print(f"answer 1 is {answer_1}") - -# part 2 - -# replace S by an appropriate character for the loop below -di1, dj1 = loop[1][0] - loop[0][0], loop[1][1] - loop[0][1] -di2, dj2 = loop[0][0] - loop[-1][0], loop[0][1] - loop[-1][1] -mapping: dict[tuple[int, int], dict[tuple[int, int], Symbol]] = { - (0, 1): {(0, 1): "-", (-1, 0): "F", (1, 0): "L"}, - (0, -1): {(0, -1): "-", (-1, 0): "7", (1, 0): "J"}, - (1, 0): {(1, 0): "|", (0, 1): "7", (0, -1): "F"}, - (-1, 0): {(-1, 0): "|", (0, -1): "L", (0, 1): "J"}, -} -lines[si][sj] = mapping[di1, dj1][di2, dj2] - -# find the points inside the loop using an adaptation of ray casting for a discrete -# grid (https://stackoverflow.com/a/218081/2666289) -# -# use a set for faster '... in loop' check -# -loop_s = set(loop) -inside: set[tuple[int, int]] = set() -for i in range(len(lines)): - cnt = 0 - for j in range(len(lines[0])): - if (i, j) not in loop_s and cnt % 2 == 1: - inside.add((i, j)) - - if (i, j) in loop_s and lines[i][j] in "|LJ": - cnt += 1 - -if VERBOSE: - for i in range(len(lines)): - for j in range(len(lines[0])): if (i, j) == (si, sj): - print("\033[91mS\033[0m", end="") - elif (i, j) in loop: - print(lines[i][j], end="") - elif (i, j) in inside: - print("\033[92mI\033[0m", end="") - else: - print(".", end="") - print() + break -answer_2 = len(inside) -print(f"answer 2 is {answer_2}") + loop.append((i, j)) + + yield len(loop) // 2 + + # part 2 + + # replace S by an appropriate character for the loop below + di1, dj1 = loop[1][0] - loop[0][0], loop[1][1] - loop[0][1] + di2, dj2 = loop[0][0] - loop[-1][0], loop[0][1] - loop[-1][1] + mapping: dict[tuple[int, int], dict[tuple[int, int], Symbol]] = { + (0, 1): {(0, 1): "-", (-1, 0): "F", (1, 0): "L"}, + (0, -1): {(0, -1): "-", (-1, 0): "7", (1, 0): "J"}, + (1, 0): {(1, 0): "|", (0, 1): "7", (0, -1): "F"}, + (-1, 0): {(-1, 0): "|", (0, -1): "L", (0, 1): "J"}, + } + lines[si][sj] = mapping[di1, dj1][di2, dj2] + + # find the points inside the loop using an adaptation of ray casting for a discrete + # grid (https://stackoverflow.com/a/218081/2666289) + # + # use a set for faster '... in loop' check + # + loop_s = set(loop) + inside: set[tuple[int, int]] = set() + for i in range(len(lines)): + cnt = 0 + for j in range(len(lines[0])): + if (i, j) not in loop_s and cnt % 2 == 1: + inside.add((i, j)) + + if (i, j) in loop_s and lines[i][j] in "|LJ": + cnt += 1 + + if self.verbose: + for i in range(len(lines)): + s = "" + for j in range(len(lines[0])): + if (i, j) == (si, sj): + s += "\033[91mS\033[0m" + elif (i, j) in loop: + s += lines[i][j] + elif (i, j) in inside: + s += "\033[92mI\033[0m" + else: + s += "." + self.logger.info(s) + + yield len(inside) diff --git a/src/holt59/aoc/2023/day11.py b/src/holt59/aoc/2023/day11.py index b18b1c9..d1f1494 100644 --- a/src/holt59/aoc/2023/day11.py +++ b/src/holt59/aoc/2023/day11.py @@ -1,41 +1,42 @@ -import sys +from typing import Any, Iterator import numpy as np -lines = sys.stdin.read().splitlines() - -data = np.array([[c == "#" for c in line] for line in lines]) - -rows = {c for c in range(data.shape[0]) if not data[c, :].any()} -columns = {c for c in range(data.shape[1]) if not data[:, c].any()} - -galaxies_y, galaxies_x = np.where(data) # type: ignore +from ..base import BaseSolver -def compute_total_distance(expansion: int) -> int: - distances: list[int] = [] - for g1 in range(len(galaxies_y)): - x1, y1 = int(galaxies_x[g1]), int(galaxies_y[g1]) - for g2 in range(g1 + 1, len(galaxies_y)): - x2, y2 = int(galaxies_x[g2]), int(galaxies_y[g2]) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() - dx = sum( - 1 + (expansion - 1) * (x in columns) - for x in range(min(x1, x2), max(x1, x2)) - ) - dy = sum( - 1 + (expansion - 1) * (y in rows) - for y in range(min(y1, y2), max(y1, y2)) - ) + data = np.array([[c == "#" for c in line] for line in lines]) - distances.append(dx + dy) - return sum(distances) + rows = {c for c in range(data.shape[0]) if not data[c, :].any()} + columns = {c for c in range(data.shape[1]) if not data[:, c].any()} + galaxies_y, galaxies_x = np.where(data) # type: ignore -# part 1 -answer_1 = compute_total_distance(2) -print(f"answer 1 is {answer_1}") + def compute_total_distance(expansion: int) -> int: + distances: list[int] = [] + for g1 in range(len(galaxies_y)): + x1, y1 = int(galaxies_x[g1]), int(galaxies_y[g1]) + for g2 in range(g1 + 1, len(galaxies_y)): + x2, y2 = int(galaxies_x[g2]), int(galaxies_y[g2]) -# part 2 -answer_2 = compute_total_distance(1000000) -print(f"answer 2 is {answer_2}") + dx = sum( + 1 + (expansion - 1) * (x in columns) + for x in range(min(x1, x2), max(x1, x2)) + ) + dy = sum( + 1 + (expansion - 1) * (y in rows) + for y in range(min(y1, y2), max(y1, y2)) + ) + + distances.append(dx + dy) + return sum(distances) + + # part 1 + yield compute_total_distance(2) + + # part 2 + yield compute_total_distance(1000000) diff --git a/src/holt59/aoc/2023/day12.py b/src/holt59/aoc/2023/day12.py index 64a98ab..150a211 100644 --- a/src/holt59/aoc/2023/day12.py +++ b/src/holt59/aoc/2023/day12.py @@ -1,9 +1,7 @@ -import os -import sys from functools import lru_cache -from typing import Iterable +from typing import Any, Iterable, Iterator -VERBOSE = os.getenv("AOC_VERBOSE") == "True" +from ..base import BaseSolver @lru_cache @@ -77,31 +75,29 @@ def compute_possible_arrangements( ) -def compute_all_possible_arrangements(lines: Iterable[str], repeat: int) -> int: - count = 0 +class Solver(BaseSolver): + def compute_all_possible_arrangements( + self, lines: Iterable[str], repeat: int + ) -> int: + count = 0 - if VERBOSE: - from tqdm import tqdm + for i_line, line in enumerate(lines): + self.logger.info(f"processing line {i_line}: {line}...") + parts = line.split(" ") + count += compute_possible_arrangements( + tuple( + filter(len, "?".join(parts[0] for _ in range(repeat)).split(".")) + ), + tuple(int(c) for c in parts[1].split(",")) * repeat, + ) - lines = tqdm(lines) + return count - for line in lines: - parts = line.split(" ") - count += compute_possible_arrangements( - tuple(filter(len, "?".join(parts[0] for _ in range(repeat)).split("."))), - tuple(int(c) for c in parts[1].split(",")) * repeat, - ) + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() - return count + # part 1 + yield self.compute_all_possible_arrangements(lines, 1) - -lines = sys.stdin.read().splitlines() - - -# part 1 -answer_1 = compute_all_possible_arrangements(lines, 1) -print(f"answer 1 is {answer_1}") - -# part 2 -answer_2 = compute_all_possible_arrangements(lines, 5) -print(f"answer 2 is {answer_2}") + # part 2 + yield self.compute_all_possible_arrangements(lines, 5) diff --git a/src/holt59/aoc/2023/day13.py b/src/holt59/aoc/2023/day13.py index b75a5ff..f437170 100644 --- a/src/holt59/aoc/2023/day13.py +++ b/src/holt59/aoc/2023/day13.py @@ -1,5 +1,6 @@ -import sys -from typing import Callable, Literal +from typing import Any, Callable, Iterator, Literal + +from ..base import BaseSolver def split(block: list[str], axis: Literal[0, 1], count: int) -> int: @@ -25,19 +26,18 @@ def split(block: list[str], axis: Literal[0, 1], count: int) -> int: return 0 -blocks = [block.splitlines() for block in sys.stdin.read().split("\n\n")] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + blocks = [block.splitlines() for block in input.split("\n\n")] + # part 1 + yield sum( + split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0) + for block in blocks + ) -# part 1 -answer_1 = sum( - split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0) - for block in blocks -) -print(f"answer 1 is {answer_1}") - -# part 2 -answer_2 = sum( - split(block, axis=1, count=1) + 100 * split(block, axis=0, count=1) - for block in blocks -) -print(f"answer 2 is {answer_2}") + # part 2 + yield sum( + split(block, axis=1, count=1) + 100 * split(block, axis=0, count=1) + for block in blocks + ) diff --git a/src/holt59/aoc/2023/day14.py b/src/holt59/aoc/2023/day14.py index 40360c0..35c30bf 100644 --- a/src/holt59/aoc/2023/day14.py +++ b/src/holt59/aoc/2023/day14.py @@ -1,10 +1,9 @@ -import sys -from typing import TypeAlias +from typing import Any, Iterator, TypeAlias + +from ..base import BaseSolver RockGrid: TypeAlias = list[list[str]] -rocks0 = [list(line) for line in sys.stdin.read().splitlines()] - def slide_rocks_top(rocks: RockGrid) -> RockGrid: top = [0 if c == "." else 1 for c in rocks[0]] @@ -34,35 +33,38 @@ def cycle(rocks: RockGrid) -> RockGrid: return rocks -rocks = slide_rocks_top([[c for c in r] for r in rocks0]) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + rocks0 = [list(line) for line in input.splitlines()] -# part 1 -answer_1 = sum( - (len(rocks) - i) * sum(1 for c in row if c == "O") for i, row in enumerate(rocks) -) -print(f"answer 1 is {answer_1}") + rocks = slide_rocks_top([[c for c in r] for r in rocks0]) -# part 2 -rocks = rocks0 + # part 1 + yield sum( + (len(rocks) - i) * sum(1 for c in row if c == "O") + for i, row in enumerate(rocks) + ) -N = 1000000000 -cycles: list[RockGrid] = [] -i_cycle: int = -1 -for i_cycle in range(N): - rocks = cycle(rocks) + # part 2 + rocks = rocks0 - if any(rocks == c for c in cycles): - break + N = 1000000000 + cycles: list[RockGrid] = [] + i_cycle: int = -1 + for i_cycle in range(N): + rocks = cycle(rocks) - cycles.append([[c for c in r] for r in rocks]) + if any(rocks == c for c in cycles): + break -cycle_start = next(i for i in range(len(cycles)) if (rocks == cycles[i])) -cycle_length = i_cycle - cycle_start + cycles.append([[c for c in r] for r in rocks]) -ci = cycle_start + (N - cycle_start) % cycle_length - 1 + cycle_start = next(i for i in range(len(cycles)) if (rocks == cycles[i])) + cycle_length = i_cycle - cycle_start -answer_2 = sum( - (len(rocks) - i) * sum(1 for c in row if c == "O") - for i, row in enumerate(cycles[ci]) -) -print(f"answer 2 is {answer_2}") + ci = cycle_start + (N - cycle_start) % cycle_length - 1 + + yield sum( + (len(rocks) - i) * sum(1 for c in row if c == "O") + for i, row in enumerate(cycles[ci]) + ) diff --git a/src/holt59/aoc/2023/day15.py b/src/holt59/aoc/2023/day15.py index bd8caad..a4189d6 100644 --- a/src/holt59/aoc/2023/day15.py +++ b/src/holt59/aoc/2023/day15.py @@ -1,31 +1,33 @@ -import sys from functools import reduce +from typing import Any, Iterator -steps = sys.stdin.read().strip().split(",") +from ..base import BaseSolver def _hash(s: str) -> int: return reduce(lambda v, u: ((v + ord(u)) * 17) % 256, s, 0) -# part 1 -answer_1 = sum(map(_hash, steps)) -print(f"answer 1 is {answer_1}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + steps = input.split(",") -# part 2 -boxes: list[dict[str, int]] = [{} for _ in range(256)] + # part 1 + yield sum(map(_hash, steps)) -for step in steps: - if (i := step.find("=")) >= 0: - label, length = step[:i], int(step[i + 1 :]) - boxes[_hash(label)][label] = length - else: - label = step[:-1] - boxes[_hash(label)].pop(label, None) + # part 2 + boxes: list[dict[str, int]] = [{} for _ in range(256)] -answer_2 = sum( - i_box * i_lens * length - for i_box, box in enumerate(boxes, start=1) - for i_lens, length in enumerate(box.values(), start=1) -) -print(f"answer 2 is {answer_2}") + for step in steps: + if (i := step.find("=")) >= 0: + label, length = step[:i], int(step[i + 1 :]) + boxes[_hash(label)][label] = length + else: + label = step[:-1] + boxes[_hash(label)].pop(label, None) + + yield sum( + i_box * i_lens * length + for i_box, box in enumerate(boxes, start=1) + for i_lens, length in enumerate(box.values(), start=1) + ) diff --git a/src/holt59/aoc/2023/day16.py b/src/holt59/aoc/2023/day16.py index 2b986e1..a307d96 100644 --- a/src/holt59/aoc/2023/day16.py +++ b/src/holt59/aoc/2023/day16.py @@ -1,8 +1,6 @@ -import os -import sys -from typing import Literal, TypeAlias, cast +from typing import Any, Iterator, Literal, TypeAlias, cast -VERBOSE = os.getenv("AOC_VERBOSE") == "True" +from ..base import BaseSolver CellType: TypeAlias = Literal[".", "|", "-", "\\", "/"] Direction: TypeAlias = Literal["R", "L", "U", "D"] @@ -78,33 +76,33 @@ def propagate( return beams -layout: list[list[CellType]] = [ - [cast(CellType, col) for col in row] for row in sys.stdin.read().splitlines() -] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + layout: list[list[CellType]] = [ + [cast(CellType, col) for col in row] for row in input.splitlines() + ] + beams = propagate(layout, (0, 0), "R") -beams = propagate(layout, (0, 0), "R") + if self.verbose: + for row in beams: + self.logger.info("".join("#" if col else "." for col in row)) -if VERBOSE: - print("\n".join(["".join("#" if col else "." for col in row) for row in beams])) + # part 1 + yield sum(sum(map(bool, row)) for row in beams) -# part 1 -answer_1 = sum(sum(map(bool, row)) for row in beams) -print(f"answer 1 is {answer_1}") + # part 2 + n_rows, n_cols = len(layout), len(layout[0]) + cases: list[tuple[tuple[int, int], Direction]] = [] -# part 2 -n_rows, n_cols = len(layout), len(layout[0]) -cases: list[tuple[tuple[int, int], Direction]] = [] + for row in range(n_rows): + cases.append(((row, 0), "R")) + cases.append(((row, n_cols - 1), "L")) + for col in range(n_cols): + cases.append(((0, col), "D")) + cases.append(((n_rows - 1, col), "U")) -for row in range(n_rows): - cases.append(((row, 0), "R")) - cases.append(((row, n_cols - 1), "L")) -for col in range(n_cols): - cases.append(((0, col), "D")) - cases.append(((n_rows - 1, col), "U")) - -answer_2 = max( - sum(sum(map(bool, row)) for row in propagate(layout, start, direction)) - for start, direction in cases -) -print(f"answer 2 is {answer_2}") + yield max( + sum(sum(map(bool, row)) for row in propagate(layout, start, direction)) + for start, direction in cases + ) diff --git a/src/holt59/aoc/2023/day17.py b/src/holt59/aoc/2023/day17.py index 250af9b..c4bd609 100644 --- a/src/holt59/aoc/2023/day17.py +++ b/src/holt59/aoc/2023/day17.py @@ -1,13 +1,11 @@ from __future__ import annotations import heapq -import os -import sys from collections import defaultdict from dataclasses import dataclass -from typing import Literal, TypeAlias +from typing import Any, Iterator, Literal, TypeAlias -VERBOSE = os.getenv("AOC_VERBOSE") == "True" +from ..base import BaseSolver Direction: TypeAlias = Literal[">", "<", "^", "v"] @@ -32,202 +30,204 @@ MAPPINGS: dict[Direction, tuple[int, int, Direction]] = { } -def print_shortest_path( - grid: list[list[int]], - target: tuple[int, int], - per_cell: dict[tuple[int, int], list[tuple[Label, int]]], -): - assert len(per_cell[target]) == 1 - label = per_cell[target][0][0] +class Solver(BaseSolver): + def print_shortest_path( + self, + grid: list[list[int]], + target: tuple[int, int], + per_cell: dict[tuple[int, int], list[tuple[Label, int]]], + ): + assert len(per_cell[target]) == 1 + label = per_cell[target][0][0] - path: list[Label] = [] - while True: - path.insert(0, label) - if label.parent is None: - break - label = label.parent + path: list[Label] = [] + while True: + path.insert(0, label) + if label.parent is None: + break + label = label.parent - p_grid = [[str(c) for c in r] for r in grid] + p_grid = [[str(c) for c in r] for r in grid] - for i in range(len(grid)): - for j in range(len(grid[0])): - if per_cell[i, j]: - p_grid[i][j] = f"\033[94m{grid[i][j]}\033[0m" + for i in range(len(grid)): + for j in range(len(grid[0])): + if per_cell[i, j]: + p_grid[i][j] = f"\033[94m{grid[i][j]}\033[0m" - prev_label = path[0] - for label in path[1:]: - for r in range( - min(prev_label.row, label.row), max(prev_label.row, label.row) + 1 - ): - for c in range( - min(prev_label.col, label.col), - max(prev_label.col, label.col) + 1, + prev_label = path[0] + for label in path[1:]: + for r in range( + min(prev_label.row, label.row), max(prev_label.row, label.row) + 1 ): - if (r, c) != (prev_label.row, prev_label.col): - p_grid[r][c] = f"\033[93m{grid[r][c]}\033[0m" + for c in range( + min(prev_label.col, label.col), + max(prev_label.col, label.col) + 1, + ): + if (r, c) != (prev_label.row, prev_label.col): + p_grid[r][c] = f"\033[93m{grid[r][c]}\033[0m" - p_grid[label.row][label.col] = f"\033[91m{grid[label.row][label.col]}\033[0m" + p_grid[label.row][label.col] = ( + f"\033[91m{grid[label.row][label.col]}\033[0m" + ) - prev_label = label + prev_label = label - p_grid[0][0] = f"\033[92m{grid[0][0]}\033[0m" + p_grid[0][0] = f"\033[92m{grid[0][0]}\033[0m" - print("\n".join("".join(row) for row in p_grid)) + for row in p_grid: + self.logger.info("".join(row)) + def shortest_many_paths(self, grid: list[list[int]]) -> dict[tuple[int, int], int]: + n_rows, n_cols = len(grid), len(grid[0]) -def shortest_many_paths(grid: list[list[int]]) -> dict[tuple[int, int], int]: - n_rows, n_cols = len(grid), len(grid[0]) + visited: dict[tuple[int, int], tuple[Label, int]] = {} - visited: dict[tuple[int, int], tuple[Label, int]] = {} + queue: list[tuple[int, Label]] = [ + (0, Label(row=n_rows - 1, col=n_cols - 1, direction="^", count=0)) + ] - queue: list[tuple[int, Label]] = [ - (0, Label(row=n_rows - 1, col=n_cols - 1, direction="^", count=0)) - ] + while queue and len(visited) != n_rows * n_cols: + distance, label = heapq.heappop(queue) - while queue and len(visited) != n_rows * n_cols: - distance, label = heapq.heappop(queue) - - if (label.row, label.col) in visited: - continue - - visited[label.row, label.col] = (label, distance) - - for direction, (c_row, c_col, i_direction) in MAPPINGS.items(): - if label.direction == i_direction: - continue - else: - row, col = (label.row + c_row, label.col + c_col) - - # exclude labels outside the grid or with too many moves in the same - # direction - if row not in range(0, n_rows) or col not in range(0, n_cols): + if (label.row, label.col) in visited: continue - heapq.heappush( - queue, - ( + visited[label.row, label.col] = (label, distance) + + for direction, (c_row, c_col, i_direction) in MAPPINGS.items(): + if label.direction == i_direction: + continue + else: + row, col = (label.row + c_row, label.col + c_col) + + # exclude labels outside the grid or with too many moves in the same + # direction + if row not in range(0, n_rows) or col not in range(0, n_cols): + continue + + heapq.heappush( + queue, + ( + distance + + sum( + grid[r][c] + for r in range(min(row, label.row), max(row, label.row) + 1) + for c in range(min(col, label.col), max(col, label.col) + 1) + ) + - grid[row][col], + Label( + row=row, + col=col, + direction=direction, + count=0, + parent=label, + ), + ), + ) + + return {(r, c): visited[r, c][1] for r in range(n_rows) for c in range(n_cols)} + + def shortest_path( + self, + grid: list[list[int]], + min_straight: int, + max_straight: int, + lower_bounds: dict[tuple[int, int], int], + ) -> int: + n_rows, n_cols = len(grid), len(grid[0]) + + target = (len(grid) - 1, len(grid[0]) - 1) + + # for each tuple (row, col, direction, count), the associated label when visited + visited: dict[tuple[int, int, str, int], Label] = {} + + # list of all visited labels for a cell (with associated distance) + per_cell: dict[tuple[int, int], list[tuple[Label, int]]] = defaultdict(list) + + # need to add two start labels, otherwise one of the two possible direction will + # not be possible + queue: list[tuple[int, int, Label]] = [ + (lower_bounds[0, 0], 0, Label(row=0, col=0, direction="^", count=0)), + (lower_bounds[0, 0], 0, Label(row=0, col=0, direction="<", count=0)), + ] + + while queue: + _, distance, label = heapq.heappop(queue) + + if (label.row, label.col, label.direction, label.count) in visited: + continue + + visited[label.row, label.col, label.direction, label.count] = label + per_cell[label.row, label.col].append((label, distance)) + + if (label.row, label.col) == target: + break + + for direction, (c_row, c_col, i_direction) in MAPPINGS.items(): + # cannot move in the opposite direction + if label.direction == i_direction: + continue + + # other direction, move 'min_straight' in the new direction + elif label.direction != direction: + row, col, count = ( + label.row + min_straight * c_row, + label.col + min_straight * c_col, + min_straight, + ) + + # same direction, too many count + elif label.count == max_straight: + continue + + # same direction, keep going and increment count + else: + row, col, count = ( + label.row + c_row, + label.col + c_col, + label.count + 1, + ) + # exclude labels outside the grid or with too many moves in the same + # direction + if row not in range(0, n_rows) or col not in range(0, n_cols): + continue + + distance_to = ( distance + sum( grid[r][c] for r in range(min(row, label.row), max(row, label.row) + 1) for c in range(min(col, label.col), max(col, label.col) + 1) ) - - grid[row][col], - Label( - row=row, - col=col, - direction=direction, - count=0, - parent=label, + - grid[label.row][label.col] + ) + + heapq.heappush( + queue, + ( + distance_to + lower_bounds[row, col], + distance_to, + Label( + row=row, + col=col, + direction=direction, + count=count, + parent=label, + ), ), - ), - ) - - return {(r, c): visited[r, c][1] for r in range(n_rows) for c in range(n_cols)} - - -def shortest_path( - grid: list[list[int]], - min_straight: int, - max_straight: int, - lower_bounds: dict[tuple[int, int], int], -) -> int: - n_rows, n_cols = len(grid), len(grid[0]) - - target = (len(grid) - 1, len(grid[0]) - 1) - - # for each tuple (row, col, direction, count), the associated label when visited - visited: dict[tuple[int, int, str, int], Label] = {} - - # list of all visited labels for a cell (with associated distance) - per_cell: dict[tuple[int, int], list[tuple[Label, int]]] = defaultdict(list) - - # need to add two start labels, otherwise one of the two possible direction will - # not be possible - queue: list[tuple[int, int, Label]] = [ - (lower_bounds[0, 0], 0, Label(row=0, col=0, direction="^", count=0)), - (lower_bounds[0, 0], 0, Label(row=0, col=0, direction="<", count=0)), - ] - - while queue: - _, distance, label = heapq.heappop(queue) - - if (label.row, label.col, label.direction, label.count) in visited: - continue - - visited[label.row, label.col, label.direction, label.count] = label - per_cell[label.row, label.col].append((label, distance)) - - if (label.row, label.col) == target: - break - - for direction, (c_row, c_col, i_direction) in MAPPINGS.items(): - # cannot move in the opposite direction - if label.direction == i_direction: - continue - - # other direction, move 'min_straight' in the new direction - elif label.direction != direction: - row, col, count = ( - label.row + min_straight * c_row, - label.col + min_straight * c_col, - min_straight, ) - # same direction, too many count - elif label.count == max_straight: - continue + if self.verbose: + self.print_shortest_path(grid, target, per_cell) - # same direction, keep going and increment count - else: - row, col, count = ( - label.row + c_row, - label.col + c_col, - label.count + 1, - ) - # exclude labels outside the grid or with too many moves in the same - # direction - if row not in range(0, n_rows) or col not in range(0, n_cols): - continue + return per_cell[target][0][1] - distance_to = ( - distance - + sum( - grid[r][c] - for r in range(min(row, label.row), max(row, label.row) + 1) - for c in range(min(col, label.col), max(col, label.col) + 1) - ) - - grid[label.row][label.col] - ) + def solve(self, input: str) -> Iterator[Any]: + data = [[int(c) for c in r] for r in input.splitlines()] + estimates = self.shortest_many_paths(data) - heapq.heappush( - queue, - ( - distance_to + lower_bounds[row, col], - distance_to, - Label( - row=row, - col=col, - direction=direction, - count=count, - parent=label, - ), - ), - ) + # part 1 + yield self.shortest_path(data, 1, 3, lower_bounds=estimates) - if VERBOSE: - print_shortest_path(grid, target, per_cell) - - return per_cell[target][0][1] - - -data = [[int(c) for c in r] for r in sys.stdin.read().splitlines()] -estimates = shortest_many_paths(data) - -# part 1 -answer_1 = shortest_path(data, 1, 3, lower_bounds=estimates) -print(f"answer 1 is {answer_1}") - -# part 2 -answer_2 = shortest_path(data, 4, 10, lower_bounds=estimates) -print(f"answer 2 is {answer_2}") + # part 2 + yield self.shortest_path(data, 4, 10, lower_bounds=estimates) diff --git a/src/holt59/aoc/2023/day18.py b/src/holt59/aoc/2023/day18.py index 6d3f5ad..89d93c5 100644 --- a/src/holt59/aoc/2023/day18.py +++ b/src/holt59/aoc/2023/day18.py @@ -1,5 +1,6 @@ -import sys -from typing import Literal, TypeAlias, cast +from typing import Any, Iterator, Literal, TypeAlias, cast + +from ..base import BaseSolver Direction: TypeAlias = Literal["R", "L", "U", "D"] @@ -33,22 +34,23 @@ def polygon(values: list[tuple[Direction, int]]) -> tuple[list[tuple[int, int]], return corners, perimeter -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() + # part 1 + yield area( + *polygon( + [(cast(Direction, (p := line.split())[0]), int(p[1])) for line in lines] + ) + ) -# part 1 -answer_1 = area( - *polygon([(cast(Direction, (p := line.split())[0]), int(p[1])) for line in lines]) -) -print(f"answer 1 is {answer_1}") - -# part 2 -answer_2 = area( - *polygon( - [ - (DIRECTIONS[int((h := line.split()[-1])[-2])], int(h[2:-2], 16)) - for line in lines - ] - ) -) -print(f"answer 2 is {answer_2}") + # part 2 + yield area( + *polygon( + [ + (DIRECTIONS[int((h := line.split()[-1])[-2])], int(h[2:-2], 16)) + for line in lines + ] + ) + ) diff --git a/src/holt59/aoc/2023/day19.py b/src/holt59/aoc/2023/day19.py index b41b326..87f795f 100644 --- a/src/holt59/aoc/2023/day19.py +++ b/src/holt59/aoc/2023/day19.py @@ -1,13 +1,8 @@ -import logging import operator -import os -import sys from math import prod -from typing import Literal, TypeAlias, cast +from typing import Any, Iterator, Literal, TypeAlias, cast -VERBOSE = os.getenv("AOC_VERBOSE") == "True" - -logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING) +from ..base import BaseSolver Category: TypeAlias = Literal["x", "m", "a", "s"] Part: TypeAlias = dict[Category, int] @@ -22,119 +17,118 @@ Check: TypeAlias = tuple[Category, Literal["<", ">"], int] | None Workflow: TypeAlias = list[tuple[Check, str]] -def accept(workflows: dict[str, Workflow], part: Part) -> bool: - workflow = "in" - decision: bool | None = None +class Solver(BaseSolver): + def accept(self, workflows: dict[str, Workflow], part: Part) -> bool: + workflow = "in" + decision: bool | None = None - while decision is None: - for check, target in workflows[workflow]: - passed = check is None - if check is not None: - category, sense, value = check - passed = OPERATORS[sense](part[category], value) + while decision is None: + for check, target in workflows[workflow]: + passed = check is None + if check is not None: + category, sense, value = check + passed = OPERATORS[sense](part[category], value) - if passed: - if target in workflows: - workflow = target - else: - decision = target == "A" - break + if passed: + if target in workflows: + workflow = target + else: + decision = target == "A" + break - return decision + return decision + def propagate(self, workflows: dict[str, Workflow], start: PartWithBounds) -> int: + def _fmt(meta: PartWithBounds) -> str: + return "{" + ", ".join(f"{k}={v}" for k, v in meta.items()) + "}" -def propagate(workflows: dict[str, Workflow], start: PartWithBounds) -> int: - def _fmt(meta: PartWithBounds) -> str: - return "{" + ", ".join(f"{k}={v}" for k, v in meta.items()) + "}" - - def transfer_or_accept( - target: str, meta: PartWithBounds, queue: list[tuple[PartWithBounds, str]] - ) -> int: - count = 0 - if target in workflows: - logging.info(f" transfer to {target}") - queue.append((meta, target)) - elif target == "A": - count = prod((high - low + 1) for low, high in meta.values()) - logging.info(f" accepted ({count})") - else: - logging.info(" rejected") - return count - - accepted = 0 - queue: list[tuple[PartWithBounds, str]] = [(start, "in")] - - n_iterations = 0 - - while queue: - n_iterations += 1 - meta, workflow = queue.pop() - logging.info(f"{workflow}: {_fmt(meta)}") - for check, target in workflows[workflow]: - if check is None: - logging.info(" end-of-workflow") - accepted += transfer_or_accept(target, meta, queue) - continue - - category, sense, value = check - bounds, op = meta[category], OPERATORS[sense] - - logging.info(f" checking {_fmt(meta)} against {category} {sense} {value}") - - if not op(bounds[0], value) and not op(bounds[1], value): - logging.info(" reject, always false") - continue - - if op(meta[category][0], value) and op(meta[category][1], value): - logging.info(" accept, always true") - accepted += transfer_or_accept(target, meta, queue) - break - - meta2 = meta.copy() - low, high = meta[category] - if sense == "<": - meta[category], meta2[category] = (value, high), (low, value - 1) + def transfer_or_accept( + target: str, meta: PartWithBounds, queue: list[tuple[PartWithBounds, str]] + ) -> int: + count = 0 + if target in workflows: + self.logger.info(f" transfer to {target}") + queue.append((meta, target)) + elif target == "A": + count = prod((high - low + 1) for low, high in meta.values()) + self.logger.info(f" accepted ({count})") else: - meta[category], meta2[category] = (low, value), (value + 1, high) - logging.info(f" split {_fmt(meta2)} ({target}), {_fmt(meta)}") + self.logger.info(" rejected") + return count - accepted += transfer_or_accept(target, meta2, queue) + accepted = 0 + queue: list[tuple[PartWithBounds, str]] = [(start, "in")] - logging.info(f"run took {n_iterations} iterations") - return accepted + n_iterations = 0 + while queue: + n_iterations += 1 + meta, workflow = queue.pop() + self.logger.info(f"{workflow}: {_fmt(meta)}") + for check, target in workflows[workflow]: + if check is None: + self.logger.info(" end-of-workflow") + accepted += transfer_or_accept(target, meta, queue) + continue -workflows_s, parts_s = sys.stdin.read().strip().split("\n\n") + category, sense, value = check + bounds, op = meta[category], OPERATORS[sense] -workflows: dict[str, Workflow] = {} -for workflow_s in workflows_s.split("\n"): - name, block_s = workflow_s.split("{") - workflows[name] = [] + self.logger.info( + f" checking {_fmt(meta)} against {category} {sense} {value}" + ) - for block in block_s[:-1].split(","): - check: Check - if (i := block.find(":")) >= 0: - check = ( - cast(Category, block[0]), - cast(Literal["<", ">"], block[1]), - int(block[2:i]), - ) - target = block[i + 1 :] - else: - check, target = None, block - workflows[name].append((check, target)) + if not op(bounds[0], value) and not op(bounds[1], value): + self.logger.info(" reject, always false") + continue -# part 1 -parts: list[Part] = [ - {cast(Category, s[0]): int(s[2:]) for s in part_s[1:-1].split(",")} - for part_s in parts_s.split("\n") -] -answer_1 = sum(sum(part.values()) for part in parts if accept(workflows, part)) -print(f"answer 1 is {answer_1}") + if op(meta[category][0], value) and op(meta[category][1], value): + self.logger.info(" accept, always true") + accepted += transfer_or_accept(target, meta, queue) + break + meta2 = meta.copy() + low, high = meta[category] + if sense == "<": + meta[category], meta2[category] = (value, high), (low, value - 1) + else: + meta[category], meta2[category] = (low, value), (value + 1, high) + self.logger.info(f" split {_fmt(meta2)} ({target}), {_fmt(meta)}") -# part 2 -answer_2 = propagate( - workflows, {cast(Category, c): (1, 4000) for c in ["x", "m", "a", "s"]} -) -print(f"answer 2 is {answer_2}") + accepted += transfer_or_accept(target, meta2, queue) + + self.logger.info(f"run took {n_iterations} iterations") + return accepted + + def solve(self, input: str) -> Iterator[Any]: + workflows_s, parts_s = input.split("\n\n") + + workflows: dict[str, Workflow] = {} + for workflow_s in workflows_s.split("\n"): + name, block_s = workflow_s.split("{") + workflows[name] = [] + + for block in block_s[:-1].split(","): + check: Check + if (i := block.find(":")) >= 0: + check = ( + cast(Category, block[0]), + cast(Literal["<", ">"], block[1]), + int(block[2:i]), + ) + target = block[i + 1 :] + else: + check, target = None, block + workflows[name].append((check, target)) + + # part 1 + parts: list[Part] = [ + {cast(Category, s[0]): int(s[2:]) for s in part_s[1:-1].split(",")} + for part_s in parts_s.split("\n") + ] + yield sum(sum(part.values()) for part in parts if self.accept(workflows, part)) + + # part 2 + yield self.propagate( + workflows, {cast(Category, c): (1, 4000) for c in ["x", "m", "a", "s"]} + ) diff --git a/src/holt59/aoc/2023/day2.py b/src/holt59/aoc/2023/day2.py index a768318..9d1a05c 100644 --- a/src/holt59/aoc/2023/day2.py +++ b/src/holt59/aoc/2023/day2.py @@ -1,43 +1,43 @@ import math -import sys -from typing import Literal, TypeAlias, cast +from typing import Any, Iterator, Literal, TypeAlias, cast + +from ..base import BaseSolver CubeType: TypeAlias = Literal["red", "blue", "green"] MAX_CUBES: dict[CubeType, int] = {"red": 12, "green": 13, "blue": 14} -# parse games -lines = sys.stdin.read().splitlines() -games: dict[int, list[dict[CubeType, int]]] = {} -for line in filter(bool, lines): - id_part, sets_part = line.split(":") - games[int(id_part.split(" ")[-1])] = [ - { - cast(CubeType, s[1]): int(s[0]) - for cube_draw in cube_set_s.strip().split(", ") - if (s := cube_draw.split(" ")) - } - for cube_set_s in sets_part.strip().split(";") - ] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() + games: dict[int, list[dict[CubeType, int]]] = {} + for line in filter(bool, lines): + id_part, sets_part = line.split(":") -# part 1 -answer_1 = sum( - id - for id, set_of_cubes in games.items() - if all( - n_cubes <= MAX_CUBES[cube] - for cube_set in set_of_cubes - for cube, n_cubes in cube_set.items() - ) -) -print(f"answer 1 is {answer_1}") + games[int(id_part.split(" ")[-1])] = [ + { + cast(CubeType, s[1]): int(s[0]) + for cube_draw in cube_set_s.strip().split(", ") + if (s := cube_draw.split(" ")) + } + for cube_set_s in sets_part.strip().split(";") + ] -# part 2 -answer_2 = sum( - math.prod( - max(cube_set.get(cube, 0) for cube_set in set_of_cubes) for cube in MAX_CUBES - ) - for set_of_cubes in games.values() -) -print(f"answer 2 is {answer_2}") + yield sum( + id + for id, set_of_cubes in games.items() + if all( + n_cubes <= MAX_CUBES[cube] + for cube_set in set_of_cubes + for cube, n_cubes in cube_set.items() + ) + ) + + yield sum( + math.prod( + max(cube_set.get(cube, 0) for cube_set in set_of_cubes) + for cube in MAX_CUBES + ) + for set_of_cubes in games.values() + ) diff --git a/src/holt59/aoc/2023/day20.py b/src/holt59/aoc/2023/day20.py index c960301..975620f 100644 --- a/src/holt59/aoc/2023/day20.py +++ b/src/holt59/aoc/2023/day20.py @@ -1,161 +1,172 @@ -import logging -import os import sys from collections import defaultdict from math import lcm -from typing import Literal, TypeAlias - -VERBOSE = os.getenv("AOC_VERBOSE") == "True" -logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING) +from typing import Any, Iterator, Literal, TypeAlias +from ..base import BaseSolver ModuleType: TypeAlias = Literal["broadcaster", "conjunction", "flip-flop"] PulseType: TypeAlias = Literal["high", "low"] -modules: dict[str, tuple[ModuleType, list[str]]] = {} -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + _modules: dict[str, tuple[ModuleType, list[str]]] -for line in lines: - name, outputs_s = line.split(" -> ") - outputs = outputs_s.split(", ") - if name == "broadcaster": - modules["broadcaster"] = ("broadcaster", outputs) - else: - modules[name[1:]] = ( - "conjunction" if name.startswith("&") else "flip-flop", - outputs, + def _process( + self, + start: tuple[str, str, PulseType], + flip_flop_states: dict[str, Literal["on", "off"]], + conjunction_states: dict[str, dict[str, PulseType]], + ) -> tuple[dict[PulseType, int], dict[str, dict[PulseType, int]]]: + pulses: list[tuple[str, str, PulseType]] = [start] + counts: dict[PulseType, int] = {"low": 0, "high": 0} + inputs: dict[str, dict[PulseType, int]] = defaultdict( + lambda: {"low": 0, "high": 0} ) + self.logger.info("starting process... ") -def process( - start: tuple[str, str, PulseType], - flip_flop_states: dict[str, Literal["on", "off"]], - conjunction_states: dict[str, dict[str, PulseType]], -) -> tuple[dict[PulseType, int], dict[str, dict[PulseType, int]]]: - pulses: list[tuple[str, str, PulseType]] = [start] - counts: dict[PulseType, int] = {"low": 0, "high": 0} - inputs: dict[str, dict[PulseType, int]] = defaultdict(lambda: {"low": 0, "high": 0}) + while pulses: + input, name, pulse = pulses.pop(0) + self.logger.info(f"{input} -{pulse}-> {name}") + counts[pulse] += 1 - logging.info("starting process... ") + inputs[name][pulse] += 1 - while pulses: - input, name, pulse = pulses.pop(0) - logging.info(f"{input} -{pulse}-> {name}") - counts[pulse] += 1 - - inputs[name][pulse] += 1 - - if name not in modules: - continue - - type, outputs = modules[name] - - if type == "broadcaster": - ... - - elif type == "flip-flop": - if pulse == "high": + if name not in self._modules: continue - if flip_flop_states[name] == "off": - flip_flop_states[name] = "on" - pulse = "high" + type, outputs = self._modules[name] + + if type == "broadcaster": + ... + + elif type == "flip-flop": + if pulse == "high": + continue + + if flip_flop_states[name] == "off": + flip_flop_states[name] = "on" + pulse = "high" + else: + flip_flop_states[name] = "off" + pulse = "low" + else: - flip_flop_states[name] = "off" - pulse = "low" + conjunction_states[name][input] = pulse - else: - conjunction_states[name][input] = pulse + if all(state == "high" for state in conjunction_states[name].values()): + pulse = "low" + else: + pulse = "high" - if all(state == "high" for state in conjunction_states[name].values()): - pulse = "low" + pulses.extend((name, output, pulse) for output in outputs) + + return counts, inputs + + def solve(self, input: str) -> Iterator[Any]: + self._modules = {} + + lines = sys.stdin.read().splitlines() + + for line in lines: + name, outputs_s = line.split(" -> ") + outputs = outputs_s.split(", ") + if name == "broadcaster": + self._modules["broadcaster"] = ("broadcaster", outputs) else: - pulse = "high" + self._modules[name[1:]] = ( + "conjunction" if name.startswith("&") else "flip-flop", + outputs, + ) - pulses.extend((name, output, pulse) for output in outputs) + if self.outputs: + with open("./day20.dot", "w") as fp: + fp.write("digraph G {\n") + fp.write("rx [shape=circle, color=red, style=filled];\n") + for name, (type, outputs) in self._modules.items(): + if type == "conjunction": + shape = "diamond" + elif type == "flip-flop": + shape = "box" + else: + shape = "circle" + fp.write(f"{name} [shape={shape}];\n") + for name, (type, outputs) in self._modules.items(): + for output in outputs: + fp.write(f"{name} -> {output};\n") + fp.write("}\n") - return counts, inputs + # part 1 + flip_flop_states: dict[str, Literal["on", "off"]] = { + name: "off" + for name, (type, _) in self._modules.items() + if type == "flip-flop" + } + conjunction_states: dict[str, dict[str, PulseType]] = { + name: { + input: "low" + for input, (_, outputs) in self._modules.items() + if name in outputs + } + for name, (type, _) in self._modules.items() + if type == "conjunction" + } + counts: dict[PulseType, int] = {"low": 0, "high": 0} + for _ in range(1000): + result, _ = self._process( + ("button", "broadcaster", "low"), flip_flop_states, conjunction_states + ) + for pulse in ("low", "high"): + counts[pulse] += result[pulse] + yield counts["low"] * counts["high"] + # part 2 -with open("./day20.dot", "w") as fp: - fp.write("digraph G {\n") - fp.write("rx [shape=circle, color=red, style=filled];\n") - for name, (type, outputs) in modules.items(): - if type == "conjunction": - shape = "diamond" - elif type == "flip-flop": - shape = "box" - else: - shape = "circle" - fp.write(f"{name} [shape={shape}];\n") - for name, (type, outputs) in modules.items(): - for output in outputs: - fp.write(f"{name} -> {output};\n") - fp.write("}\n") + # reset states + for name in flip_flop_states: + flip_flop_states[name] = "off" -# part 1 -flip_flop_states: dict[str, Literal["on", "off"]] = { - name: "off" for name, (type, _) in modules.items() if type == "flip-flop" -} -conjunction_states: dict[str, dict[str, PulseType]] = { - name: {input: "low" for input, (_, outputs) in modules.items() if name in outputs} - for name, (type, _) in modules.items() - if type == "conjunction" -} -counts: dict[PulseType, int] = {"low": 0, "high": 0} -for _ in range(1000): - result, _ = process( - ("button", "broadcaster", "low"), flip_flop_states, conjunction_states - ) - for pulse in ("low", "high"): - counts[pulse] += result[pulse] -answer_1 = counts["low"] * counts["high"] -print(f"answer 1 is {answer_1}") + for name in conjunction_states: + for input in conjunction_states[name]: + conjunction_states[name][input] = "low" -# part 2 + # find the conjunction connected to rx + to_rx = [ + name for name, (_, outputs) in self._modules.items() if "rx" in outputs + ] + assert len(to_rx) == 1, "cannot handle multiple module inputs for rx" + assert ( + self._modules[to_rx[0]][0] == "conjunction" + ), "can only handle conjunction as input to rx" -# reset states -for name in flip_flop_states: - flip_flop_states[name] = "off" + to_rx_inputs = [ + name for name, (_, outputs) in self._modules.items() if to_rx[0] in outputs + ] + assert all( + self._modules[i][0] == "conjunction" and len(self._modules[i][1]) == 1 + for i in to_rx_inputs + ), "can only handle inversion as second-order inputs to rx" -for name in conjunction_states: - for input in conjunction_states[name]: - conjunction_states[name][input] = "low" + count = 1 + cycles: dict[str, int] = {} + second: dict[str, int] = {} + while len(second) != len(to_rx_inputs): + _, inputs = self._process( + ("button", "broadcaster", "low"), flip_flop_states, conjunction_states + ) -# find the conjunction connected to rx -to_rx = [name for name, (_, outputs) in modules.items() if "rx" in outputs] -assert len(to_rx) == 1, "cannot handle multiple module inputs for rx" -assert ( - modules[to_rx[0]][0] == "conjunction" -), "can only handle conjunction as input to rx" + for node in to_rx_inputs: + if inputs[node]["low"] == 1: + if node not in cycles: + cycles[node] = count + elif node not in second: + second[node] = count -to_rx_inputs = [name for name, (_, outputs) in modules.items() if to_rx[0] in outputs] -assert all( - modules[i][0] == "conjunction" and len(modules[i][1]) == 1 for i in to_rx_inputs -), "can only handle inversion as second-order inputs to rx" + count += 1 + assert all( + second[k] == cycles[k] * 2 for k in to_rx_inputs + ), "cannot only handle cycles starting at the beginning" -count = 1 -cycles: dict[str, int] = {} -second: dict[str, int] = {} -while len(second) != len(to_rx_inputs): - _, inputs = process( - ("button", "broadcaster", "low"), flip_flop_states, conjunction_states - ) - - for node in to_rx_inputs: - if inputs[node]["low"] == 1: - if node not in cycles: - cycles[node] = count - elif node not in second: - second[node] = count - - count += 1 - -assert all( - second[k] == cycles[k] * 2 for k in to_rx_inputs -), "cannot only handle cycles starting at the beginning" - -answer_2 = lcm(*cycles.values()) -print(f"answer 2 is {answer_2}") + yield lcm(*cycles.values()) diff --git a/src/holt59/aoc/2023/day21.py b/src/holt59/aoc/2023/day21.py index c44a4c1..a525751 100644 --- a/src/holt59/aoc/2023/day21.py +++ b/src/holt59/aoc/2023/day21.py @@ -1,9 +1,6 @@ -import logging -import os -import sys +from typing import Any, Iterator -VERBOSE = os.getenv("AOC_VERBOSE") == "True" -logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING) +from ..base import BaseSolver def reachable( @@ -21,129 +18,133 @@ def reachable( return tiles -map = sys.stdin.read().splitlines() -start = next( - (i, j) for i in range(len(map)) for j in range(len(map[i])) if map[i][j] == "S" -) - -# part 1 -answer_1 = len(reachable(map, {start}, 6 if len(map) < 20 else 64)) -print(f"answer 1 is {answer_1}") - -# part 2 - -# the initial map is a square and contains an empty rhombus whose diameter is the size -# of the map, and has only empty cells around the middle row and column -# -# after ~n/2 steps, the first map is filled with a rhombus, after that we get a bigger -# rhombus every n steps -# -# we are going to find the number of cells reached for the initial rhombus, n steps -# after and n * 2 steps after -# -cycle = len(map) -rhombus = (len(map) - 3) // 2 + 1 - -values: list[int] = [] -values.append(len(tiles := reachable(map, {start}, rhombus))) -values.append(len(tiles := reachable(map, tiles, cycle))) -values.append(len(tiles := reachable(map, tiles, cycle))) - -if logging.root.getEffectiveLevel() == logging.INFO: - n_rows, n_cols = len(map), len(map[0]) - - rows = [ - [ - map[i % n_rows][j % n_cols] if (i, j) not in tiles else "O" - for j in range(-2 * cycle, 3 * cycle) - ] - for i in range(-2 * cycle, 3 * cycle) - ] - - for i in range(len(rows)): - for j in range(len(rows[i])): - if (i // cycle) % 2 == (j // cycle) % 2: - rows[i][j] = f"\033[91m{rows[i][j]}\033[0m" - - print("\n".join("".join(row) for row in rows)) - - -logging.info(f"values to fit: {values}") - -# version 1: -# -# after 3 cycles, the figure looks like the following: -# -# I M D -# I J A K D -# H A F A L -# C E A K B -# C G B -# -# after 4 cycles, the figure looks like the following: -# -# I M D -# I J A K D -# I J A B A K D -# H A B A B A L -# C E A B A N F -# C E A N F -# C G F -# -# the 'radius' of the rhombus is the number of cycles minus 1 -# -# the 4 'corner' (M, H, L, G) are counted once, the blocks with a corner triangle (D, I, -# C, B) are each counted radius times, the blocks with everything but one corner (J, K, -# E, N) are each counted radius - 1 times -# -# there are two versions of the whole block, A and B in the above (or odd and even), -# depending on the number of cycles, either A or B will be in the center -# - -counts = [ - [ - sum( - (i, j) in tiles - for i in range(ci * cycle, (ci + 1) * cycle) - for j in range(cj * cycle, (cj + 1) * cycle) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + map = input.splitlines() + start = next( + (i, j) + for i in range(len(map)) + for j in range(len(map[i])) + if map[i][j] == "S" ) - for cj in range(-2, 3) - ] - for ci in range(-2, 3) -] -radius = (26501365 - rhombus) // cycle - 1 -A = counts[2][2] if radius % 2 == 0 else counts[2][1] -B = counts[2][2] if radius % 2 == 1 else counts[2][1] -answer_2 = ( - (radius + 1) * A - + radius * B - + 2 * radius * (radius + 1) // 2 * A - + 2 * radius * (radius - 1) // 2 * B - + sum(counts[i][j] for i, j in ((0, 2), (-1, 2), (2, 0), (2, -1))) - + sum(counts[i][j] for i, j in ((0, 1), (0, 3), (-1, 1), (-1, 3))) * (radius + 1) - + sum(counts[i][j] for i, j in ((1, 1), (1, 3), (-2, 1), (-2, 3))) * radius -) -print(f"answer 2 (v1) is {answer_2}") + # part 1 + yield len(reachable(map, {start}, 6 if len(map) < 20 else 64)) -# version 2: fitting a polynomial -# -# the value we are interested in (26501365) can be written as R + K * C where R is the -# step at which we find the first rhombus, and K the repeat step, so instead of fitting -# for X values (R, R + K, R + 2 K), we are going to fit for (0, 1, 2), giving us much -# simpler equation for the a, b and c coefficient -# -# we get: -# - (a * 0Ā² + b * 0 + c) = y1 => c = y1 -# - (a * 1Ā² + b * 1 + c) = y2 => a + b = y2 - y1 -# => b = y2 - y1 - a -# - (a * 2Ā² + b * 2 + c) = y3 => 4a + 2b = y3 - y1 -# => 4a + 2(y2 - y1 - a) = y3 - y1 -# => a = (y1 + y3) / 2 - y2 -# -y1, y2, y3 = values -a, b, c = (y1 + y3) // 2 - y2, 2 * y2 - (3 * y1 + y3) // 2, y1 + # part 2 -n = (26501365 - rhombus) // cycle -answer_2 = a * n * n + b * n + c -print(f"answer 2 (v2) is {answer_2}") + # the initial map is a square and contains an empty rhombus whose diameter is + # the size of the map, and has only empty cells around the middle row and column + # + # after ~n/2 steps, the first map is filled with a rhombus, after that we get a + # bigger rhombus every n steps + # + # we are going to find the number of cells reached for the initial rhombus, n + # steps after and n * 2 steps after + # + cycle = len(map) + rhombus = (len(map) - 3) // 2 + 1 + + values: list[int] = [] + values.append(len(tiles := reachable(map, {start}, rhombus))) + values.append(len(tiles := reachable(map, tiles, cycle))) + values.append(len(tiles := reachable(map, tiles, cycle))) + + if self.verbose: + n_rows, n_cols = len(map), len(map[0]) + + rows = [ + [ + map[i % n_rows][j % n_cols] if (i, j) not in tiles else "O" + for j in range(-2 * cycle, 3 * cycle) + ] + for i in range(-2 * cycle, 3 * cycle) + ] + + for i in range(len(rows)): + for j in range(len(rows[i])): + if (i // cycle) % 2 == (j // cycle) % 2: + rows[i][j] = f"\033[91m{rows[i][j]}\033[0m" + + for row in rows: + self.logger.info("".join(row)) + + self.logger.info(f"values to fit: {values}") + + # version 1: + # + # after 3 cycles, the figure looks like the following: + # + # I M D + # I J A K D + # H A F A L + # C E A K B + # C G B + # + # after 4 cycles, the figure looks like the following: + # + # I M D + # I J A K D + # I J A B A K D + # H A B A B A L + # C E A B A N F + # C E A N F + # C G F + # + # the 'radius' of the rhombus is the number of cycles minus 1 + # + # the 4 'corner' (M, H, L, G) are counted once, the blocks with a corner triangle (D, I, + # C, B) are each counted radius times, the blocks with everything but one corner (J, K, + # E, N) are each counted radius - 1 times + # + # there are two versions of the whole block, A and B in the above (or odd and even), + # depending on the number of cycles, either A or B will be in the center + # + + counts = [ + [ + sum( + (i, j) in tiles + for i in range(ci * cycle, (ci + 1) * cycle) + for j in range(cj * cycle, (cj + 1) * cycle) + ) + for cj in range(-2, 3) + ] + for ci in range(-2, 3) + ] + + radius = (26501365 - rhombus) // cycle - 1 + A = counts[2][2] if radius % 2 == 0 else counts[2][1] + B = counts[2][2] if radius % 2 == 1 else counts[2][1] + answer_2 = ( + (radius + 1) * A + + radius * B + + 2 * radius * (radius + 1) // 2 * A + + 2 * radius * (radius - 1) // 2 * B + + sum(counts[i][j] for i, j in ((0, 2), (-1, 2), (2, 0), (2, -1))) + + sum(counts[i][j] for i, j in ((0, 1), (0, 3), (-1, 1), (-1, 3))) + * (radius + 1) + + sum(counts[i][j] for i, j in ((1, 1), (1, 3), (-2, 1), (-2, 3))) * radius + ) + print(f"answer 2 (v1) is {answer_2}") + + # version 2: fitting a polynomial + # + # the value we are interested in (26501365) can be written as R + K * C where R is the + # step at which we find the first rhombus, and K the repeat step, so instead of fitting + # for X values (R, R + K, R + 2 K), we are going to fit for (0, 1, 2), giving us much + # simpler equation for the a, b and c coefficient + # + # we get: + # - (a * 0Ā² + b * 0 + c) = y1 => c = y1 + # - (a * 1Ā² + b * 1 + c) = y2 => a + b = y2 - y1 + # => b = y2 - y1 - a + # - (a * 2Ā² + b * 2 + c) = y3 => 4a + 2b = y3 - y1 + # => 4a + 2(y2 - y1 - a) = y3 - y1 + # => a = (y1 + y3) / 2 - y2 + # + y1, y2, y3 = values + a, b, c = (y1 + y3) // 2 - y2, 2 * y2 - (3 * y1 + y3) // 2, y1 + + n = (26501365 - rhombus) // cycle + yield a * n * n + b * n + c diff --git a/src/holt59/aoc/2023/day22.py b/src/holt59/aoc/2023/day22.py index 64199ea..aeed6b7 100644 --- a/src/holt59/aoc/2023/day22.py +++ b/src/holt59/aoc/2023/day22.py @@ -1,111 +1,109 @@ import itertools -import logging -import os import string -import sys from collections import defaultdict +from typing import Any, Iterator -VERBOSE = os.getenv("AOC_VERBOSE") == "True" -logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING) +from ..base import BaseSolver -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() + def _name(i: int) -> str: + if len(lines) < 26: + return string.ascii_uppercase[i] + return f"B{i:04d}" -def _name(i: int) -> str: - if len(lines) < 26: - return string.ascii_uppercase[i] - return f"B{i:04d}" + def build_supports( + bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]], + ) -> tuple[dict[int, set[int]], dict[int, set[int]]]: + # 1. compute locations where a brick of sand will land after falling by processing + # them in sorted order of bottom z location + levels: dict[tuple[int, int, int], int] = defaultdict(lambda: -1) + for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks): + assert sx <= ex and sy <= ey and sz <= ez + xs, ys = range(sx, ex + 1), range(sy, ey + 1) -def build_supports( - bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]], -) -> tuple[dict[int, set[int]], dict[int, set[int]]]: - # 1. compute locations where a brick of sand will land after falling by processing - # them in sorted order of bottom z location - levels: dict[tuple[int, int, int], int] = defaultdict(lambda: -1) - for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks): - assert sx <= ex and sy <= ey and sz <= ez + for z in range(sz - 1, 0, -1): + if any(levels[x, y, z] >= 0 for x, y in itertools.product(xs, ys)): + break + sz, ez = sz - 1, ez - 1 - xs, ys = range(sx, ex + 1), range(sy, ey + 1) + bricks[i_brick] = ((sx, sy, sz), (ex, ey, ez)) + zs = range(sz, ez + 1) - for z in range(sz - 1, 0, -1): - if any(levels[x, y, z] >= 0 for x, y in itertools.product(xs, ys)): - break - sz, ez = sz - 1, ez - 1 + for x, y, z in itertools.product(xs, ys, zs): + levels[x, y, z] = i_brick - bricks[i_brick] = ((sx, sy, sz), (ex, ey, ez)) - zs = range(sz, ez + 1) + # 2. compute the bricks that supports any brick + supported_by: dict[int, set[int]] = {} + supports: dict[int, set[int]] = { + i_brick: set() for i_brick in range(len(bricks)) + } + for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks): + name = _name(i_brick) - for x, y, z in itertools.product(xs, ys, zs): - levels[x, y, z] = i_brick + supported_by[i_brick] = { + v + for x, y in itertools.product(range(sx, ex + 1), range(sy, ey + 1)) + if (v := levels[x, y, sz - 1]) != -1 + } + self.logger.info( + f"{name} supported by {', '.join(map(_name, supported_by[i_brick]))}" + ) - # 2. compute the bricks that supports any brick - supported_by: dict[int, set[int]] = {} - supports: dict[int, set[int]] = {i_brick: set() for i_brick in range(len(bricks))} - for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks): - name = _name(i_brick) + for support in supported_by[i_brick]: + supports[support].add(i_brick) - supported_by[i_brick] = { - v - for x, y in itertools.product(range(sx, ex + 1), range(sy, ey + 1)) - if (v := levels[x, y, sz - 1]) != -1 - } - logging.info( - f"{name} supported by {', '.join(map(_name, supported_by[i_brick]))}" + return supported_by, supports + + bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]] = [] + for line in lines: + bricks.append( + ( + tuple(int(c) for c in line.split("~")[0].split(",")), # type: ignore + tuple(int(c) for c in line.split("~")[1].split(",")), # type: ignore + ) + ) + + # sort bricks by bottom z position to compute supports + bricks = sorted(bricks, key=lambda b: b[0][-1]) + supported_by, supports = build_supports(bricks) + + # part 1 + yield len(bricks) - sum( + any(len(supported_by[supported]) == 1 for supported in supports_to) + for supports_to in supports.values() ) - for support in supported_by[i_brick]: - supports[support].add(i_brick) + # part 2 + falling_in_chain: dict[int, set[int]] = {} + for i_brick in range(len(bricks)): + to_disintegrate: set[int] = { + supported + for supported in supports[i_brick] + if len(supported_by[supported]) == 1 + } - return supported_by, supports + supported_by_copy = dict(supported_by) + falling_in_chain[i_brick] = set() + while to_disintegrate: + falling_in_chain[i_brick].update(to_disintegrate) -bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]] = [] -for line in lines: - bricks.append( - ( - tuple(int(c) for c in line.split("~")[0].split(",")), # type: ignore - tuple(int(c) for c in line.split("~")[1].split(",")), # type: ignore - ) - ) + to_disintegrate_v: set[int] = set() -# sort bricks by bottom z position to compute supports -bricks = sorted(bricks, key=lambda b: b[0][-1]) -supported_by, supports = build_supports(bricks) + for d_brick in to_disintegrate: + for supported in supports[d_brick]: + supported_by_copy[supported] = supported_by_copy[supported] - { + d_brick + } -# part 1 -answer_1 = len(bricks) - sum( - any(len(supported_by[supported]) == 1 for supported in supports_to) - for supports_to in supports.values() -) -print(f"answer 1 is {answer_1}") + if not supported_by_copy[supported]: + to_disintegrate_v.add(supported) -# part 2 -falling_in_chain: dict[int, set[int]] = {} -for i_brick in range(len(bricks)): - to_disintegrate: set[int] = { - supported - for supported in supports[i_brick] - if len(supported_by[supported]) == 1 - } + to_disintegrate = to_disintegrate_v - supported_by_copy = dict(supported_by) - - falling_in_chain[i_brick] = set() - while to_disintegrate: - falling_in_chain[i_brick].update(to_disintegrate) - - to_disintegrate_v: set[int] = set() - - for d_brick in to_disintegrate: - for supported in supports[d_brick]: - supported_by_copy[supported] = supported_by_copy[supported] - {d_brick} - - if not supported_by_copy[supported]: - to_disintegrate_v.add(supported) - - to_disintegrate = to_disintegrate_v - -answer_2 = sum(len(falling) for falling in falling_in_chain.values()) -print(f"answer 2 is {answer_2}") + yield sum(len(falling) for falling in falling_in_chain.values()) diff --git a/src/holt59/aoc/2023/day23.py b/src/holt59/aoc/2023/day23.py index dd0e135..2f8c3b2 100644 --- a/src/holt59/aoc/2023/day23.py +++ b/src/holt59/aoc/2023/day23.py @@ -1,11 +1,7 @@ -import logging -import os -import sys from collections import defaultdict -from typing import Literal, Sequence, TypeAlias, cast +from typing import Any, Iterator, Literal, Sequence, TypeAlias, cast -VERBOSE = os.getenv("AOC_VERBOSE") == "True" -logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING) +from ..base import BaseSolver DirectionType: TypeAlias = Literal[">", "<", "^", "v", ".", "#"] @@ -35,6 +31,7 @@ def neighbors( Compute neighbors of the given node, ignoring the given set of nodes and considering that you can go uphill on slopes. """ + n_rows, n_cols = len(grid), len(grid[0]) i, j = node for di, dj in Neighbors[grid[i][j]]: @@ -103,65 +100,66 @@ def compute_direct_links( return direct -def longest_path_length( - links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]], - start: tuple[int, int], - target: tuple[int, int], -) -> int: - max_distance: int = -1 - queue: list[tuple[tuple[int, int], int, frozenset[tuple[int, int]]]] = [ - (start, 0, frozenset({start})) - ] +class Solver(BaseSolver): + def longest_path_length( + self, + links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]], + start: tuple[int, int], + target: tuple[int, int], + ) -> int: + max_distance: int = -1 + queue: list[tuple[tuple[int, int], int, frozenset[tuple[int, int]]]] = [ + (start, 0, frozenset({start})) + ] - nodes = 0 - while queue: - node, distance, path = queue.pop() + nodes = 0 + while queue: + node, distance, path = queue.pop() - nodes += 1 + nodes += 1 - if node == target: - max_distance = max(distance, max_distance) - continue + if node == target: + max_distance = max(distance, max_distance) + continue - queue.extend( - (reach, distance + length, path | {reach}) - for reach, length in links.get(node, []) - if reach not in path + queue.extend( + (reach, distance + length, path | {reach}) + for reach, length in links.get(node, []) + if reach not in path + ) + + self.logger.info(f"processed {nodes} nodes") + + return max_distance + + def solve(self, input: str) -> Iterator[Any]: + lines = cast(list[Sequence[DirectionType]], input.splitlines()) + + start = (0, 1) + target = (len(lines) - 1, len(lines[0]) - 2) + + direct_links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = { + start: [reachable(lines, start, target)] + } + direct_links.update( + compute_direct_links(lines, direct_links[start][0][0], target) ) - logging.info(f"processed {nodes} nodes") + # part 1 + yield self.longest_path_length(direct_links, start, target) - return max_distance + # part 2 + reverse_links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = ( + defaultdict(list) + ) + for origin, links in direct_links.items(): + for destination, distance in links: + if origin != start: + reverse_links[destination].append((origin, distance)) + links = { + k: direct_links.get(k, []) + reverse_links.get(k, []) + for k in direct_links.keys() | reverse_links.keys() + } -lines = cast(list[Sequence[DirectionType]], sys.stdin.read().splitlines()) -n_rows, n_cols = len(lines), len(lines[0]) -start = (0, 1) -target = (len(lines) - 1, len(lines[0]) - 2) - - -direct_links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = { - start: [reachable(lines, start, target)] -} -direct_links.update(compute_direct_links(lines, direct_links[start][0][0], target)) - -# part 1 -answer_1 = longest_path_length(direct_links, start, target) -print(f"answer 1 is {answer_1}") - -# part 2 -reverse_links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = defaultdict( - list -) -for origin, links in direct_links.items(): - for destination, distance in links: - if origin != start: - reverse_links[destination].append((origin, distance)) - -links = { - k: direct_links.get(k, []) + reverse_links.get(k, []) - for k in direct_links.keys() | reverse_links.keys() -} - -answer_2 = longest_path_length(links, start, target) -print(f"answer 2 is {answer_2}") + yield self.longest_path_length(links, start, target) diff --git a/src/holt59/aoc/2023/day24.py b/src/holt59/aoc/2023/day24.py index 9564fa1..c37d32b 100644 --- a/src/holt59/aoc/2023/day24.py +++ b/src/holt59/aoc/2023/day24.py @@ -1,63 +1,68 @@ -import sys +from typing import Any, Iterator import numpy as np from sympy import solve, symbols -lines = sys.stdin.read().splitlines() - -positions = np.array( - [[int(c) for c in line.split("@")[0].strip().split(", ")] for line in lines] -) -velocities = np.array( - [[int(c) for c in line.split("@")[1].strip().split(", ")] for line in lines] -) - -# part 1 -low, high = [7, 27] if len(positions) <= 10 else [200000000000000, 400000000000000] - -count = 0 -for i1, (p1, v1) in enumerate(zip(positions, velocities)): - p, r = p1[:2], v1[:2] - - q, s = positions[i1 + 1 :, :2], velocities[i1 + 1 :, :2] - - rs = np.cross(r, s) - - q, s, rs = q[m := (rs != 0)], s[m], rs[m] - t = np.cross((q - p), s) / rs - u = np.cross((q - p), r) / rs - - t, u = t[m := ((t >= 0) & (u >= 0))], u[m] - c = p + np.expand_dims(t, 1) * r - count += np.all((low <= c) & (c <= high), axis=1).sum() +from ..base import BaseSolver -answer_1 = count -print(f"answer 1 is {answer_1}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -# part 2 -# equation -# p1 + t1 * v1 == p0 + t1 * v0 -# p2 + t2 * v2 == p0 + t2 * v0 -# p3 + t3 * v3 == p0 + t3 * v0 -# ... -# pn + tn * vn == p0 + tn * v0 -# + positions = np.array( + [[int(c) for c in line.split("@")[0].strip().split(", ")] for line in lines] + ) + velocities = np.array( + [[int(c) for c in line.split("@")[1].strip().split(", ")] for line in lines] + ) -# we can solve with only 3 lines since each lines contains 3 -# equations (x / y / z), so 3 lines give 9 equations and 9 -# variables: position (3), velocities (3) and times (3). -n = 3 + # part 1 + low, high = ( + [7, 27] if len(positions) <= 10 else [200000000000000, 400000000000000] + ) -x, y, z, vx, vy, vz, *ts = symbols( - "x y z vx vy vz " + " ".join(f"t{i}" for i in range(n + 1)) -) -equations = [] -for i1, ti in zip(range(n), ts): - for p, d, pi, di in zip((x, y, z), (vx, vy, vz), positions[i1], velocities[i1]): - equations.append(p + ti * d - pi - ti * di) + count = 0 + for i1, (p1, v1) in enumerate(zip(positions, velocities)): + p, r = p1[:2], v1[:2] -r = solve(equations, [x, y, z, vx, vy, vz] + list(ts), dict=True)[0] + q, s = positions[i1 + 1 :, :2], velocities[i1 + 1 :, :2] -answer_2 = r[x] + r[y] + r[z] -print(f"answer 2 is {answer_2}") + rs = np.cross(r, s) + + q, s, rs = q[m := (rs != 0)], s[m], rs[m] + t = np.cross((q - p), s) / rs + u = np.cross((q - p), r) / rs + + t, u = t[m := ((t >= 0) & (u >= 0))], u[m] + c = p + np.expand_dims(t, 1) * r + count += np.all((low <= c) & (c <= high), axis=1).sum() + + yield count + + # part 2 + # equation + # p1 + t1 * v1 == p0 + t1 * v0 + # p2 + t2 * v2 == p0 + t2 * v0 + # p3 + t3 * v3 == p0 + t3 * v0 + # ... + # pn + tn * vn == p0 + tn * v0 + # + + # we can solve with only 3 lines since each lines contains 3 + # equations (x / y / z), so 3 lines give 9 equations and 9 + # variables: position (3), velocities (3) and times (3). + n = 3 + + x, y, z, vx, vy, vz, *ts = symbols( + "x y z vx vy vz " + " ".join(f"t{i}" for i in range(n + 1)) + ) + equations = [] + for i1, ti in zip(range(n), ts): + for p, d, pi, di in zip( + (x, y, z), (vx, vy, vz), positions[i1], velocities[i1] + ): + equations.append(p + ti * d - pi - ti * di) + + r = solve(equations, [x, y, z, vx, vy, vz] + list(ts), dict=True)[0] + yield r[x] + r[y] + r[z] diff --git a/src/holt59/aoc/2023/day25.py b/src/holt59/aoc/2023/day25.py index 9addbf8..0f80ab7 100644 --- a/src/holt59/aoc/2023/day25.py +++ b/src/holt59/aoc/2023/day25.py @@ -1,25 +1,25 @@ -import sys +# pyright: reportUnknownMemberType=false + +from typing import Any, Iterator import networkx as nx -components = { - (p := line.split(": "))[0]: p[1].split() for line in sys.stdin.read().splitlines() -} +from ..base import BaseSolver -targets = {t for c in components for t in components[c] if t not in components} -graph = nx.Graph() -graph.add_edges_from((u, v) for u, vs in components.items() for v in vs) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + components = { + (p := line.split(": "))[0]: p[1].split() for line in input.splitlines() + } -cut = nx.minimum_edge_cut(graph) -graph.remove_edges_from(cut) + graph: "nx.Graph[str]" = nx.Graph() + graph.add_edges_from((u, v) for u, vs in components.items() for v in vs) -c1, c2 = nx.connected_components(graph) + cut = nx.minimum_edge_cut(graph) + graph.remove_edges_from(cut) -# part 1 -answer_1 = len(c1) * len(c2) -print(f"answer 1 is {answer_1}") + c1, c2 = nx.connected_components(graph) -# part 2 -answer_2 = ... -print(f"answer 2 is {answer_2}") + # part 1 + yield len(c1) * len(c2) diff --git a/src/holt59/aoc/2023/day3.py b/src/holt59/aoc/2023/day3.py index db1fff4..ae72053 100644 --- a/src/holt59/aoc/2023/day3.py +++ b/src/holt59/aoc/2023/day3.py @@ -1,53 +1,53 @@ import string -import sys from collections import defaultdict +from typing import Any, Iterator + +from ..base import BaseSolver NOT_A_SYMBOL = "." + string.digits -lines = sys.stdin.read().splitlines() -values: list[int] = [] -gears: dict[tuple[int, int], list[int]] = defaultdict(list) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -for i, line in enumerate(lines): - j = 0 - while j < len(line): - # skip everything until a digit is found (start of a number) - if line[j] not in string.digits: - j += 1 - continue + values: list[int] = [] + gears: dict[tuple[int, int], list[int]] = defaultdict(list) - # extract the range of the number and its value - k = j + 1 - while k < len(line) and line[k] in string.digits: - k += 1 + for i, line in enumerate(lines): + j = 0 + while j < len(line): + # skip everything until a digit is found (start of a number) + if line[j] not in string.digits: + j += 1 + continue - value = int(line[j:k]) + # extract the range of the number and its value + k = j + 1 + while k < len(line) and line[k] in string.digits: + k += 1 - # lookup around the number if there is a symbol - we go through the number - # itself but that should not matter since it only contains digits - found = False - for i2 in range(max(0, i - 1), min(i + 1, len(lines) - 1) + 1): - for j2 in range(max(0, j - 1), min(k, len(line) - 1) + 1): - assert i2 >= 0 and i2 < len(lines) - assert j2 >= 0 and j2 < len(line) + value = int(line[j:k]) - if lines[i2][j2] not in NOT_A_SYMBOL: - found = True + # lookup around the number if there is a symbol - we go through the number + # itself but that should not matter since it only contains digits + found = False + for i2 in range(max(0, i - 1), min(i + 1, len(lines) - 1) + 1): + for j2 in range(max(0, j - 1), min(k, len(line) - 1) + 1): + assert i2 >= 0 and i2 < len(lines) + assert j2 >= 0 and j2 < len(line) - if lines[i2][j2] == "*": - gears[i2, j2].append(value) + if lines[i2][j2] not in NOT_A_SYMBOL: + found = True - if found: - values.append(value) + if lines[i2][j2] == "*": + gears[i2, j2].append(value) - # continue starting from the end of the number - j = k + if found: + values.append(value) -# part 1 -answer_1 = sum(values) -print(f"answer 1 is {answer_1}") + # continue starting from the end of the number + j = k -# part 2 -answer_2 = sum(v1 * v2 for v1, v2 in filter(lambda vs: len(vs) == 2, gears.values())) -print(f"answer 2 is {answer_2}") + yield sum(values) + yield sum(v1 * v2 for v1, v2 in filter(lambda vs: len(vs) == 2, gears.values())) diff --git a/src/holt59/aoc/2023/day4.py b/src/holt59/aoc/2023/day4.py index 3cc9d6c..a29caac 100644 --- a/src/holt59/aoc/2023/day4.py +++ b/src/holt59/aoc/2023/day4.py @@ -1,5 +1,7 @@ -import sys from dataclasses import dataclass +from typing import Any, Iterator + +from ..base import BaseSolver @dataclass(frozen=True) @@ -9,33 +11,34 @@ class Card: values: list[int] -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -cards: list[Card] = [] -for line in lines: - id_part, e_part = line.split(":") - numbers_s, values_s = e_part.split("|") - cards.append( - Card( - id=int(id_part.split()[1]), - numbers=[int(v.strip()) for v in numbers_s.strip().split()], - values=[int(v.strip()) for v in values_s.strip().split()], - ) - ) + cards: list[Card] = [] + for line in lines: + id_part, e_part = line.split(":") + numbers_s, values_s = e_part.split("|") + cards.append( + Card( + id=int(id_part.split()[1]), + numbers=[int(v.strip()) for v in numbers_s.strip().split()], + values=[int(v.strip()) for v in values_s.strip().split()], + ) + ) -winnings = [sum(1 for n in card.values if n in card.numbers) for card in cards] + winnings = [sum(1 for n in card.values if n in card.numbers) for card in cards] -# part 1 -answer_1 = sum(2 ** (winning - 1) for winning in winnings if winning > 0) -print(f"answer 1 is {answer_1}") + # part 1 + yield sum(2 ** (winning - 1) for winning in winnings if winning > 0) -# part 2 -card2cards = {i: list(range(i + 1, i + w + 1)) for i, w in enumerate(winnings)} -card2values = {i: 0 for i in range(len(cards))} + # part 2 + card2cards = {i: list(range(i + 1, i + w + 1)) for i, w in enumerate(winnings)} + card2values = {i: 0 for i in range(len(cards))} -for i in range(len(cards)): - card2values[i] += 1 - for j in card2cards[i]: - card2values[j] += card2values[i] + for i in range(len(cards)): + card2values[i] += 1 + for j in card2cards[i]: + card2values[j] += card2values[i] -print(f"answer 2 is {sum(card2values.values())}") + yield sum(card2values.values()) diff --git a/src/holt59/aoc/2023/day5.py b/src/holt59/aoc/2023/day5.py index 33d84c5..8cac8f9 100644 --- a/src/holt59/aoc/2023/day5.py +++ b/src/holt59/aoc/2023/day5.py @@ -1,5 +1,6 @@ -import sys -from typing import Sequence +from typing import Any, Iterator, Sequence + +from ..base import BaseSolver MAP_ORDER = [ "seed", @@ -12,55 +13,6 @@ MAP_ORDER = [ "location", ] -lines = sys.stdin.read().splitlines() - -# mappings from one category to another, each list contains -# ranges stored as (source, target, length), ordered by start and -# completed to have no "hole" -maps: dict[tuple[str, str], list[tuple[int, int, int]]] = {} - -# parsing -index = 2 -while index < len(lines): - p1, _, p2 = lines[index].split()[0].split("-") - - # extract the existing ranges from the file - we store as (source, target, length) - # whereas the file is in order (target, source, length) - index += 1 - values: list[tuple[int, int, int]] = [] - while index < len(lines) and lines[index]: - n1, n2, n3 = lines[index].split() - values.append((int(n2), int(n1), int(n3))) - index += 1 - - # sort by source value - values.sort() - - # add a 'fake' interval starting at 0 if missing - if values[0][0] != 0: - values.insert(0, (0, 0, values[0][0])) - - # fill gaps between intervals - for i in range(len(values) - 1): - next_start = values[i + 1][0] - end = values[i][0] + values[i][2] - if next_start != end: - values.insert( - i + 1, - (end, end, next_start - end), - ) - - # add an interval covering values up to at least 2**32 at the end - last_start, _, last_length = values[-1] - values.append((last_start + last_length, last_start + last_length, 2**32)) - - assert all(v1[0] + v1[2] == v2[0] for v1, v2 in zip(values[:-1], values[1:])) - assert values[0][0] == 0 - assert values[-1][0] + values[-1][-1] >= 2**32 - - maps[p1, p2] = values - index += 1 - def find_range( values: tuple[int, int], map: list[tuple[int, int, int]] @@ -111,19 +63,71 @@ def find_range( return ranges -def find_location_ranges(seeds: Sequence[tuple[int, int]]) -> Sequence[tuple[int, int]]: - for map1, map2 in zip(MAP_ORDER[:-1], MAP_ORDER[1:]): - seeds = [s2 for s1 in seeds for s2 in find_range(s1, maps[map1, map2])] - return seeds +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() + # mappings from one category to another, each list contains + # ranges stored as (source, target, length), ordered by start and + # completed to have no "hole" + maps: dict[tuple[str, str], list[tuple[int, int, int]]] = {} -# part 1 - use find_range() with range of length 1 -seeds_p1 = [(int(s), 1) for s in lines[0].split(":")[1].strip().split()] -answer_1 = min(start for start, _ in find_location_ranges(seeds_p1)) -print(f"answer 1 is {answer_1}") + def find_location_ranges( + seeds: Sequence[tuple[int, int]], + ) -> Sequence[tuple[int, int]]: + for map1, map2 in zip(MAP_ORDER[:-1], MAP_ORDER[1:]): + seeds = [s2 for s1 in seeds for s2 in find_range(s1, maps[map1, map2])] + return seeds -# # part 2 -parts = lines[0].split(":")[1].strip().split() -seeds_p2 = [(int(s), int(e)) for s, e in zip(parts[::2], parts[1::2])] -answer_2 = min(start for start, _ in find_location_ranges(seeds_p2)) -print(f"answer 2 is {answer_2}") + # parsing + index = 2 + while index < len(lines): + p1, _, p2 = lines[index].split()[0].split("-") + + # extract the existing ranges from the file - we store as (source, target, length) + # whereas the file is in order (target, source, length) + index += 1 + values: list[tuple[int, int, int]] = [] + while index < len(lines) and lines[index]: + n1, n2, n3 = lines[index].split() + values.append((int(n2), int(n1), int(n3))) + index += 1 + + # sort by source value + values.sort() + + # add a 'fake' interval starting at 0 if missing + if values[0][0] != 0: + values.insert(0, (0, 0, values[0][0])) + + # fill gaps between intervals + for i in range(len(values) - 1): + next_start = values[i + 1][0] + end = values[i][0] + values[i][2] + if next_start != end: + values.insert( + i + 1, + (end, end, next_start - end), + ) + + # add an interval covering values up to at least 2**32 at the end + last_start, _, last_length = values[-1] + values.append((last_start + last_length, last_start + last_length, 2**32)) + + assert all( + v1[0] + v1[2] == v2[0] for v1, v2 in zip(values[:-1], values[1:]) + ) + assert values[0][0] == 0 + assert values[-1][0] + values[-1][-1] >= 2**32 + + maps[p1, p2] = values + index += 1 + + # part 1 - use find_range() with range of length 1 + seeds_p1 = [(int(s), 1) for s in lines[0].split(":")[1].strip().split()] + yield min(start for start, _ in find_location_ranges(seeds_p1)) + + # # part 2 + parts = lines[0].split(":")[1].strip().split() + seeds_p2 = [(int(s), int(e)) for s, e in zip(parts[::2], parts[1::2])] + yield min(start for start, _ in find_location_ranges(seeds_p2)) diff --git a/src/holt59/aoc/2023/day6.py b/src/holt59/aoc/2023/day6.py index 3290b8a..42086e9 100644 --- a/src/holt59/aoc/2023/day6.py +++ b/src/holt59/aoc/2023/day6.py @@ -1,5 +1,7 @@ import math -import sys +from typing import Any, Iterator + +from ..base import BaseSolver def extreme_times_to_beat(time: int, distance: int) -> tuple[int, int]: @@ -25,23 +27,23 @@ def extreme_times_to_beat(time: int, distance: int) -> tuple[int, int]: return t1, t2 -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -# part 1 -times = list(map(int, lines[0].split()[1:])) -distances = list(map(int, lines[1].split()[1:])) -answer_1 = math.prod( - t2 - t1 + 1 - for t1, t2 in ( - extreme_times_to_beat(time, distance) - for time, distance in zip(times, distances) - ) -) -print(f"answer 1 is {answer_1}") + # part 1 + times = list(map(int, lines[0].split()[1:])) + distances = list(map(int, lines[1].split()[1:])) + yield math.prod( + t2 - t1 + 1 + for t1, t2 in ( + extreme_times_to_beat(time, distance) + for time, distance in zip(times, distances) + ) + ) -# part 2 -time = int(lines[0].split(":")[1].strip().replace(" ", "")) -distance = int(lines[1].split(":")[1].strip().replace(" ", "")) -t1, t2 = extreme_times_to_beat(time, distance) -answer_2 = t2 - t1 + 1 -print(f"answer 2 is {answer_2}") + # part 2 + time = int(lines[0].split(":")[1].strip().replace(" ", "")) + distance = int(lines[1].split(":")[1].strip().replace(" ", "")) + t1, t2 = extreme_times_to_beat(time, distance) + yield t2 - t1 + 1 diff --git a/src/holt59/aoc/2023/day7.py b/src/holt59/aoc/2023/day7.py index b2f97b8..8647fcf 100644 --- a/src/holt59/aoc/2023/day7.py +++ b/src/holt59/aoc/2023/day7.py @@ -1,5 +1,7 @@ -import sys from collections import Counter, defaultdict +from typing import Any, Iterator + +from ..base import BaseSolver class HandTypes: @@ -32,18 +34,17 @@ def extract_key(hand: str, values: dict[str, int], joker: str = "0") -> tuple[in ) -lines = sys.stdin.read().splitlines() -cards = [(t[0], int(t[1])) for line in lines if (t := line.split())] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() + cards = [(t[0], int(t[1])) for line in lines if (t := line.split())] + # part 1 + values = {card: value for value, card in enumerate("23456789TJQKA")} + cards.sort(key=lambda cv: extract_key(cv[0], values=values)) + yield sum(rank * value for rank, (_, value) in enumerate(cards, start=1)) -# part 1 -values = {card: value for value, card in enumerate("23456789TJQKA")} -cards.sort(key=lambda cv: extract_key(cv[0], values=values)) -answer_1 = sum(rank * value for rank, (_, value) in enumerate(cards, start=1)) -print(f"answer 1 is {answer_1}") - -# part 2 -values = {card: value for value, card in enumerate("J23456789TQKA")} -cards.sort(key=lambda cv: extract_key(cv[0], values=values, joker="J")) -answer_2 = sum(rank * value for rank, (_, value) in enumerate(cards, start=1)) -print(f"answer 2 is {answer_2}") + # part 2 + values = {card: value for value, card in enumerate("J23456789TQKA")} + cards.sort(key=lambda cv: extract_key(cv[0], values=values, joker="J")) + yield sum(rank * value for rank, (_, value) in enumerate(cards, start=1)) diff --git a/src/holt59/aoc/2023/day8.py b/src/holt59/aoc/2023/day8.py index 0f16a6c..023e4f9 100644 --- a/src/holt59/aoc/2023/day8.py +++ b/src/holt59/aoc/2023/day8.py @@ -1,29 +1,30 @@ import itertools import math -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() - -sequence = lines[0] -nodes = { - p[0]: {d: n for d, n in zip("LR", p[1].strip("()").split(", "))} - for line in lines[2:] - if (p := line.split(" = ")) -} +from ..base import BaseSolver -def path(start: str): - path = [start] - it_seq = iter(itertools.cycle(sequence)) - while not path[-1].endswith("Z"): - path.append(nodes[path[-1]][next(it_seq)]) - return path +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() + sequence = lines[0] + nodes = { + p[0]: {d: n for d, n in zip("LR", p[1].strip("()").split(", "))} + for line in lines[2:] + if (p := line.split(" = ")) + } -# part 1 -answer_1 = len(path(next(node for node in nodes if node.endswith("A")))) - 1 -print(f"answer 1 is {answer_1}") + def path(start: str): + path = [start] + it_seq = iter(itertools.cycle(sequence)) + while not path[-1].endswith("Z"): + path.append(nodes[path[-1]][next(it_seq)]) + return path -# part 2 -answer_2 = math.lcm(*(len(path(node)) - 1 for node in nodes if node.endswith("A"))) -print(f"answer 2 is {answer_2}") + # part 1 + yield len(path(next(node for node in nodes if node.endswith("A")))) - 1 + + # part 2 + yield math.lcm(*(len(path(node)) - 1 for node in nodes if node.endswith("A"))) diff --git a/src/holt59/aoc/2023/day9.py b/src/holt59/aoc/2023/day9.py index 8289d5b..4d4a2d7 100644 --- a/src/holt59/aoc/2023/day9.py +++ b/src/holt59/aoc/2023/day9.py @@ -1,29 +1,34 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -data = [[int(c) for c in line.split()] for line in lines] -right_values: list[int] = [] -left_values: list[int] = [] -for values in data: - diffs = [values] - while any(d != 0 for d in diffs[-1]): - diffs.append([rhs - lhs for lhs, rhs in zip(diffs[-1][:-1], diffs[-1][1:])]) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() - rhs: list[int] = [0] - lhs: list[int] = [0] - for cx in range(len(diffs) - 1): - rhs.append(diffs[-cx - 2][-1] + rhs[cx]) - lhs.append(diffs[-cx - 2][0] - lhs[cx]) + data = [[int(c) for c in line.split()] for line in lines] - right_values.append(rhs[-1]) - left_values.append(lhs[-1]) + right_values: list[int] = [] + left_values: list[int] = [] + for values in data: + diffs = [values] + while any(d != 0 for d in diffs[-1]): + diffs.append( + [rhs - lhs for lhs, rhs in zip(diffs[-1][:-1], diffs[-1][1:])] + ) -# part 1 -answer_1 = sum(right_values) -print(f"answer 1 is {answer_1}") + rhs: list[int] = [0] + lhs: list[int] = [0] + for cx in range(len(diffs) - 1): + rhs.append(diffs[-cx - 2][-1] + rhs[cx]) + lhs.append(diffs[-cx - 2][0] - lhs[cx]) -# part 2 -answer_2 = sum(left_values) -print(f"answer 2 is {answer_2}") + right_values.append(rhs[-1]) + left_values.append(lhs[-1]) + + # part 1 + yield sum(right_values) + + # part 2 + yield sum(left_values) diff --git a/src/holt59/aoc/2024/day1.py b/src/holt59/aoc/2024/day1.py index acbf0c2..181e2a7 100644 --- a/src/holt59/aoc/2024/day1.py +++ b/src/holt59/aoc/2024/day1.py @@ -1,14 +1,17 @@ -import sys from collections import Counter +from typing import Any, Iterator -values = list(map(int, sys.stdin.read().strip().split())) +from ..base import BaseSolver -column_1 = sorted(values[::2]) -column_2 = sorted(values[1::2]) -counter_2 = Counter(column_2) -answer_1 = sum(abs(v1 - v2) for v1, v2 in zip(column_1, column_2, strict=True)) -answer_2 = sum(value * counter_2.get(value, 0) for value in column_1) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + values = list(map(int, input.split())) -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") + column_1 = sorted(values[::2]) + column_2 = sorted(values[1::2]) + + yield sum(abs(v1 - v2) for v1, v2 in zip(column_1, column_2, strict=True)) + + counter_2 = Counter(column_2) + yield sum(value * counter_2.get(value, 0) for value in column_1) diff --git a/src/holt59/aoc/2024/day10.py b/src/holt59/aoc/2024/day10.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day10.py +++ b/src/holt59/aoc/2024/day10.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day11.py b/src/holt59/aoc/2024/day11.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day11.py +++ b/src/holt59/aoc/2024/day11.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day12.py b/src/holt59/aoc/2024/day12.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day12.py +++ b/src/holt59/aoc/2024/day12.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day13.py b/src/holt59/aoc/2024/day13.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day13.py +++ b/src/holt59/aoc/2024/day13.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day14.py b/src/holt59/aoc/2024/day14.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day14.py +++ b/src/holt59/aoc/2024/day14.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day15.py b/src/holt59/aoc/2024/day15.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day15.py +++ b/src/holt59/aoc/2024/day15.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day16.py b/src/holt59/aoc/2024/day16.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day16.py +++ b/src/holt59/aoc/2024/day16.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day17.py b/src/holt59/aoc/2024/day17.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day17.py +++ b/src/holt59/aoc/2024/day17.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day18.py b/src/holt59/aoc/2024/day18.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day18.py +++ b/src/holt59/aoc/2024/day18.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day19.py b/src/holt59/aoc/2024/day19.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day19.py +++ b/src/holt59/aoc/2024/day19.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day2.py b/src/holt59/aoc/2024/day2.py index 1425d49..ff59110 100644 --- a/src/holt59/aoc/2024/day2.py +++ b/src/holt59/aoc/2024/day2.py @@ -1,22 +1,23 @@ -import sys +from typing import Any, Iterator + +from ..base import BaseSolver -def is_safe(level: list[int]) -> bool: - diff = [a - b for a, b in zip(level[:-1], level[1:], strict=True)] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + def is_safe(level: list[int]) -> bool: + diff = [a - b for a, b in zip(level[:-1], level[1:], strict=True)] - return sum(d > 0 for d in diff) in (0, len(diff)) and all( - 1 <= abs(d) <= 3 for d in diff - ) + return sum(d > 0 for d in diff) in (0, len(diff)) and all( + 1 <= abs(d) <= 3 for d in diff + ) + def is_any_safe(level: list[int]) -> bool: + return any( + is_safe(level[:i] + level[i + 1 :]) for i in range(0, len(level)) + ) -def is_any_safe(level: list[int]) -> bool: - return any(is_safe(level[:i] + level[i + 1 :]) for i in range(0, len(level))) + levels = [[int(c) for c in r.split()] for r in input.splitlines()] - -levels = [[int(c) for c in r.split()] for r in sys.stdin.read().strip().splitlines()] - -answer_1 = sum(is_safe(level) for level in levels) -answer_2 = sum(is_safe(level) or is_any_safe(level) for level in levels) - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") + yield sum(is_safe(level) for level in levels) + yield sum(is_safe(level) or is_any_safe(level) for level in levels) diff --git a/src/holt59/aoc/2024/day20.py b/src/holt59/aoc/2024/day20.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day20.py +++ b/src/holt59/aoc/2024/day20.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day21.py b/src/holt59/aoc/2024/day21.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day21.py +++ b/src/holt59/aoc/2024/day21.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day22.py b/src/holt59/aoc/2024/day22.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day22.py +++ b/src/holt59/aoc/2024/day22.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day23.py b/src/holt59/aoc/2024/day23.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day23.py +++ b/src/holt59/aoc/2024/day23.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day24.py b/src/holt59/aoc/2024/day24.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day24.py +++ b/src/holt59/aoc/2024/day24.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day25.py b/src/holt59/aoc/2024/day25.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day25.py +++ b/src/holt59/aoc/2024/day25.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/2024/day3.py b/src/holt59/aoc/2024/day3.py index 09afee0..ce2fc61 100644 --- a/src/holt59/aoc/2024/day3.py +++ b/src/holt59/aoc/2024/day3.py @@ -1,34 +1,30 @@ import re -import sys -from typing import Iterator +from typing import Any, Iterator + +from ..base import BaseSolver -def extract_multiply(line: str) -> Iterator[int]: - for m in re.finditer(r"mul\(([0-9]{1,3}),\s*([0-9]{1,3})\)", line): - yield int(m.group(1)) * int(m.group(2)) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + def extract_multiply(line: str) -> Iterator[int]: + for m in re.finditer(r"mul\(([0-9]{1,3}),\s*([0-9]{1,3})\)", line): + yield int(m.group(1)) * int(m.group(2)) + def valid_memory_blocks(line: str) -> Iterator[str]: + accumulate = True + while line: + if accumulate: + if (dont_i := line.find("don't()")) != -1: + yield line[:dont_i] + line, accumulate = line[dont_i:], False + else: + yield line + line = "" + else: + if (do_i := line.find("do()")) != -1: + line, accumulate = line[do_i:], True + else: + line = "" -def valid_memory_blocks(line: str) -> Iterator[str]: - accumulate = True - while line: - if accumulate: - if (dont_i := line.find("don't()")) != -1: - yield line[:dont_i] - line, accumulate = line[dont_i:], False - else: - yield line - line = "" - else: - if (do_i := line.find("do()")) != -1: - line, accumulate = line[do_i:], True - else: - line = "" - - -line = sys.stdin.read().strip() - -answer_1 = sum(extract_multiply(line)) -answer_2 = sum(sum(extract_multiply(block)) for block in valid_memory_blocks(line)) - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") + yield sum(extract_multiply(input)) + yield sum(sum(extract_multiply(block)) for block in valid_memory_blocks(input)) diff --git a/src/holt59/aoc/2024/day4.py b/src/holt59/aoc/2024/day4.py index 656ed2e..56cd80b 100644 --- a/src/holt59/aoc/2024/day4.py +++ b/src/holt59/aoc/2024/day4.py @@ -1,31 +1,37 @@ import itertools as it -import sys +from typing import Any, Iterator -lines = sys.stdin.read().strip().splitlines() -n = len(lines) +from ..base import BaseSolver -answer_1 = sum( - line.count("XMAS") + line.count("SAMX") - for i in range(n) - for ri, rk, ro, ci, ck, cm in ( - (1, 0, 0, 0, 1, n), - (0, 1, 0, 1, 0, n), - (0, 1, 0, 1, 1, n - i), - (0, -1, -1, 1, 1, n - i), - (1, 1, 0, 0, 1, n - i if i != 0 else 0), - (-1, -1, -1, 0, 1, n - i if i != 0 else 0), - ) - if ( - line := "".join(lines[ri * i + rk * k + ro][ci * i + ck * k] for k in range(cm)) - ) -) -answer_2 = sum( - lines[i][j] == "A" - and "".join(lines[i + di][j + dj] for di, dj in it.product((-1, 1), (-1, 1))) - in {"MSMS", "SSMM", "MMSS", "SMSM"} - for i, j in it.product(range(1, n - 1), range(1, n - 1)) -) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() + n = len(lines) -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") + yield sum( + line.count("XMAS") + line.count("SAMX") + for i in range(n) + for ri, rk, ro, ci, ck, cm in ( + (1, 0, 0, 0, 1, n), + (0, 1, 0, 1, 0, n), + (0, 1, 0, 1, 1, n - i), + (0, -1, -1, 1, 1, n - i), + (1, 1, 0, 0, 1, n - i if i != 0 else 0), + (-1, -1, -1, 0, 1, n - i if i != 0 else 0), + ) + if ( + line := "".join( + lines[ri * i + rk * k + ro][ci * i + ck * k] for k in range(cm) + ) + ) + ) + + yield sum( + lines[i][j] == "A" + and "".join( + lines[i + di][j + dj] for di, dj in it.product((-1, 1), (-1, 1)) + ) + in {"MSMS", "SSMM", "MMSS", "SMSM"} + for i, j in it.product(range(1, n - 1), range(1, n - 1)) + ) diff --git a/src/holt59/aoc/2024/day5.py b/src/holt59/aoc/2024/day5.py index 83d14b4..f601fa5 100644 --- a/src/holt59/aoc/2024/day5.py +++ b/src/holt59/aoc/2024/day5.py @@ -1,5 +1,7 @@ -import sys from collections import defaultdict +from typing import Any, Iterator + +from ..base import BaseSolver def in_correct_order(update: list[int], requirements: dict[int, set[int]]) -> bool: @@ -39,26 +41,25 @@ def to_correct_order( return update -part1, part2 = sys.stdin.read().strip().split("\n\n") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + part1, part2 = input.split("\n\n") -requirements: dict[int, set[int]] = defaultdict(set) -for line in part1.splitlines(): - v1, v2 = line.split("|") - requirements[int(v2)].add(int(v1)) + requirements: dict[int, set[int]] = defaultdict(set) + for line in part1.splitlines(): + v1, v2 = line.split("|") + requirements[int(v2)].add(int(v1)) -updates = [list(map(int, line.split(","))) for line in part2.splitlines()] + updates = [list(map(int, line.split(","))) for line in part2.splitlines()] -answer_1 = sum( - update[len(update) // 2] - for update in updates - if in_correct_order(update, requirements) -) + yield sum( + update[len(update) // 2] + for update in updates + if in_correct_order(update, requirements) + ) -answer_2 = sum( - to_correct_order(update, requirements, len(update) // 2 + 1)[-1] - for update in updates - if not in_correct_order(update, requirements) -) - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") + yield sum( + to_correct_order(update, requirements, len(update) // 2 + 1)[-1] + for update in updates + if not in_correct_order(update, requirements) + ) diff --git a/src/holt59/aoc/2024/day6.py b/src/holt59/aoc/2024/day6.py index dd55d89..34f1d51 100644 --- a/src/holt59/aoc/2024/day6.py +++ b/src/holt59/aoc/2024/day6.py @@ -1,6 +1,7 @@ import itertools as it -import sys -from typing import TypeAlias +from typing import Any, Iterator, TypeAlias + +from ..base import BaseSolver NodeType: TypeAlias = tuple[tuple[int, int], tuple[int, int]] EdgesType: TypeAlias = dict[NodeType, tuple[NodeType, set[tuple[int, int]]]] @@ -91,44 +92,31 @@ def is_loop(lines: list[str], edges: EdgesType, position: tuple[int, int]): return current_node in found -def print_grid( - lines: list[str], marked: set[tuple[int, int]], current_pos: tuple[int, int] | None -): - chars = list(map(list, lines)) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + # read lines + lines = input.splitlines() - for i, j in marked: - chars[i][j] = "X" + # find and delete original position + start_pos = next( + (i, j) + for i, row in enumerate(lines) + for j, col in enumerate(row) + if col == "^" + ) + lines[start_pos[0]] = lines[start_pos[0]].replace("^", ".") - if current_pos: - chars[current_pos[0]][current_pos[1]] = "T" + # compute edges from the map + edges = compute_graph(lines, (start_pos, (-1, 0))) - for line in chars: - print("".join(line)) - print() + # part 1 + marked: set[tuple[int, int]] = set() + current_node = START_NODE + while current_node[0] != FINAL_POS: + current_node, current_marked = edges[current_node] + marked = marked.union(current_marked) -# read lines -lines = sys.stdin.read().splitlines() + yield len(marked) -# find and delete original position -start_pos = next( - (i, j) for i, row in enumerate(lines) for j, col in enumerate(row) if col == "^" -) -lines[start_pos[0]] = lines[start_pos[0]].replace("^", ".") - -# compute edges from the map -edges = compute_graph(lines, (start_pos, (-1, 0))) - -# part 1 -marked: set[tuple[int, int]] = set() -current_node = START_NODE - -while current_node[0] != FINAL_POS: - current_node, current_marked = edges[current_node] - marked = marked.union(current_marked) - -answer_1 = len(marked) -print(f"answer 1 is {answer_1}") - -answer_2 = sum(is_loop(lines, edges, pos) for pos in marked if pos != start_pos) -print(f"answer 2 is {answer_2}") + yield sum(is_loop(lines, edges, pos) for pos in marked if pos != start_pos) diff --git a/src/holt59/aoc/2024/day7.py b/src/holt59/aoc/2024/day7.py index 4022929..5070f06 100644 --- a/src/holt59/aoc/2024/day7.py +++ b/src/holt59/aoc/2024/day7.py @@ -1,10 +1,50 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... +def evaluate( + target: int, numbers: list[int], concatenate: bool = False, current: int = 0 +) -> bool: + if not numbers: + return current == target -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") + if current > target: + return False + + head, *tail = numbers + + if evaluate(target, tail, concatenate, current + head) or evaluate( + target, tail, concatenate, current * head + ): + return True + + if not concatenate: + return False + + return evaluate(target, tail, concatenate, int(str(current) + str(head))) + + +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + targets = { + int(part[0]): list(map(int, part[1].strip().split())) + for line in input.splitlines() + if (part := line.split(":")) + } + + yield sum( + target + for target, numbers in self.progress.wrap( + targets.items(), total=len(targets) + ) + if evaluate(target, numbers) + ) + + yield sum( + target + for target, numbers in self.progress.wrap( + targets.items(), total=len(targets) + ) + if evaluate(target, numbers, True) + ) diff --git a/src/holt59/aoc/2024/day8.py b/src/holt59/aoc/2024/day8.py index 4022929..8c67a2d 100644 --- a/src/holt59/aoc/2024/day8.py +++ b/src/holt59/aoc/2024/day8.py @@ -1,10 +1,76 @@ -import sys +import itertools as it +from collections import defaultdict +from typing import Any, Iterator, cast -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... +def compute_antinodes( + a1: tuple[int, int], + a2: tuple[int, int], + n_rows: int, + n_cols: int, + min_distance: int = 1, + max_distance: int | None = 1, +): + if a1[0] > a2[0]: + a1, a2 = a2, a1 -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") + d_row, d_col = a2[0] - a1[0], a2[1] - a1[1] + + points: list[tuple[int, int]] = [] + + for c in range(min_distance, (max_distance or n_rows) + 1): + row_1, col_1 = a1[0] - c * d_row, a1[1] - c * d_col + row_2, col_2 = a2[0] + c * d_row, a2[1] + c * d_col + + valid_1, valid_2 = ( + 0 <= row_1 < n_rows and 0 <= col_1 < n_cols, + 0 <= row_2 < n_rows and 0 <= col_2 < n_cols, + ) + + if not valid_1 and not valid_2: + break + + if valid_1: + points.append((row_1, col_1)) + if valid_2: + points.append((row_2, col_2)) + + return tuple(points) + + +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() + + n_rows, n_cols = len(lines), len(lines[0]) + + antennas: dict[str, list[tuple[int, int]]] = defaultdict(list) + for i, j in it.product(range(n_rows), range(n_cols)): + if lines[i][j] != ".": + antennas[lines[i][j]].append((i, j)) + + yield len( + cast(set[tuple[int, int]], set()).union( + it.chain( + *( + compute_antinodes(a1, a2, n_rows, n_cols) + for antennas_of_frequency in antennas.values() + for a1, a2 in it.permutations(antennas_of_frequency, 2) + ) + ) + ) + ) + + yield len( + cast(set[tuple[int, int]], set()).union( + it.chain( + *( + compute_antinodes(a1, a2, n_rows, n_cols, 0, None) + for antennas_of_frequency in antennas.values() + for a1, a2 in it.permutations(antennas_of_frequency, 2) + ) + ) + ) + ) diff --git a/src/holt59/aoc/2024/day9.py b/src/holt59/aoc/2024/day9.py index 4022929..07e201e 100644 --- a/src/holt59/aoc/2024/day9.py +++ b/src/holt59/aoc/2024/day9.py @@ -1,10 +1,7 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -answer_1 = ... -answer_2 = ... - -print(f"answer 1 is {answer_1}") -print(f"answer 2 is {answer_2}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: ... diff --git a/src/holt59/aoc/__main__.py b/src/holt59/aoc/__main__.py index 88ddc40..edf9b06 100644 --- a/src/holt59/aoc/__main__.py +++ b/src/holt59/aoc/__main__.py @@ -1,14 +1,114 @@ import argparse import importlib -import os +import json +import logging +import logging.handlers import sys +from datetime import datetime, timedelta from pathlib import Path +from typing import Any, Iterable, Iterator, Literal, Sequence, TextIO, TypeVar + +from tqdm import tqdm + +from .base import BaseSolver + +_T = TypeVar("_T") + + +def dump_api_message( + type: Literal["log", "answer", "progress-start", "progress-step", "progress-end"], + content: Any, + file: TextIO = sys.stdout, +): + print( + json.dumps( + {"type": type, "time": datetime.now().isoformat(), "content": content} + ), + flush=True, + file=file, + ) + + +class LoggerAPIHandler(logging.Handler): + def __init__(self, output: TextIO = sys.stdout): + super().__init__() + self.output = output + + def emit(self, record: logging.LogRecord): + dump_api_message( + "log", {"level": record.levelname, "message": record.getMessage()} + ) + + +class ProgressAPI: + def __init__( + self, + min_step: int = 1, + min_time: timedelta = timedelta(milliseconds=100), + output: TextIO = sys.stdout, + ): + super().__init__() + + self.counter = 0 + self.output = output + self.min_step = min_step + self.min_time = min_time + + def wrap( + self, values: Sequence[_T] | Iterable[_T], total: int | None = None + ) -> Iterator[_T]: + total = total or len(values) # type: ignore + + current = self.counter + self.counter += 1 + + dump_api_message("progress-start", {"counter": current, "total": total}) + + try: + percent = 0 + time = datetime.now() + + for i_value, value in enumerate(values): + yield value + + if datetime.now() - time < self.min_time: + continue + + time = datetime.now() + + c_percent = round(i_value / total * 100) + + if c_percent >= percent + self.min_step: + dump_api_message( + "progress-step", {"counter": current, "percent": c_percent} + ) + percent = c_percent + finally: + dump_api_message( + "progress-end", + {"counter": current}, + ) + + +class ProgressTQDM: + def wrap( + self, values: Sequence[_T] | Iterable[_T], total: int | None = None + ) -> Iterator[_T]: + return iter(tqdm(values, total=total)) + + +class ProgressNone: + def wrap( + self, values: Sequence[_T] | Iterable[_T], total: int | None = None + ) -> Iterator[_T]: + return iter(values) def main(): parser = argparse.ArgumentParser("Holt59 Advent-Of-Code Runner") parser.add_argument("-v", "--verbose", action="store_true", help="verbose mode") parser.add_argument("-t", "--test", action="store_true", help="test mode") + parser.add_argument("-a", "--api", action="store_true", help="API mode") parser.add_argument( "-u", "--user", type=str, default="holt59", help="user input to use" ) @@ -31,6 +131,7 @@ def main(): args = parser.parse_args() verbose: bool = args.verbose + api: bool = args.api test: bool = args.test stdin: bool = args.stdin user: str = args.user @@ -40,8 +141,10 @@ def main(): day: int = args.day # TODO: change this - if verbose: - os.environ["AOC_VERBOSE"] = "True" + logging.basicConfig( + level=logging.INFO if verbose or api else logging.WARNING, + handlers=[LoggerAPIHandler()] if api else None, + ) if input_path is None: input_path = Path(__file__).parent.joinpath( @@ -49,11 +152,55 @@ def main(): ) assert input_path.exists(), f"{input_path} missing" + solver_class: type[BaseSolver] = importlib.import_module( + f".{year}.day{day}", __package__ + ).Solver + + solver = solver_class( + logging.getLogger("AOC"), + verbose=verbose, + year=year, + day=day, + progress=ProgressAPI() + if api + else ProgressTQDM() + if verbose + else ProgressNone(), # type: ignore + outputs=not api, + ) + + data: str if stdin: - importlib.import_module(f".{year}.day{day}", __package__) + data = sys.stdin.read() else: with open(input_path) as fp: - sys.stdin = fp - importlib.import_module(f".{year}.day{day}", __package__) + data = fp.read() - sys.stdin = sys.__stdin__ + start = datetime.now() + last = start + + it = solver.solve(data.rstrip()) + + if it is None: + solver.logger.error(f"no implementation for {year} day {day}") + exit() + + for i_answer, answer in enumerate(it): + current = datetime.now() + + if api: + dump_api_message( + "answer", + { + "answer": i_answer + 1, + "value": answer, + "answerTime_s": (current - last).total_seconds(), + "totalTime_s": (current - start).total_seconds(), + }, + ) + else: + print( + f"answer {i_answer + 1} is {answer} (found in {(current - last).total_seconds():.2f}s)" + ) + + last = current diff --git a/src/holt59/aoc/base.py b/src/holt59/aoc/base.py new file mode 100644 index 0000000..5ba8ada --- /dev/null +++ b/src/holt59/aoc/base.py @@ -0,0 +1,34 @@ +from abc import abstractmethod +from logging import Logger +from typing import Any, Final, Iterable, Iterator, Protocol, Sequence, TypeVar, overload + +_T = TypeVar("_T") + + +class ProgressHandler(Protocol): + @overload + def wrap(self, values: Sequence[_T]) -> Iterator[_T]: ... + + @overload + def wrap(self, values: Iterable[_T], total: int) -> Iterator[_T]: ... + + +class BaseSolver: + def __init__( + self, + logger: Logger, + verbose: bool, + year: int, + day: int, + progress: ProgressHandler, + outputs: bool = False, + ): + self.logger: Final = logger + self.verbose: Final = verbose + self.year: Final = year + self.day: Final = day + self.progress: Final = progress + self.outputs = outputs + + @abstractmethod + def solve(self, input: str) -> Iterator[Any] | None: ... diff --git a/src/holt59/aoc/inputs/holt59/2024/day7.txt b/src/holt59/aoc/inputs/holt59/2024/day7.txt index e69de29..b8cb530 100644 --- a/src/holt59/aoc/inputs/holt59/2024/day7.txt +++ b/src/holt59/aoc/inputs/holt59/2024/day7.txt @@ -0,0 +1,850 @@ +408407: 40 770 70 8 1 +1222314027: 4 40 6 66 2 4 7 8 5 519 +79632370: 12 6 79 8 369 1 +173138715990: 7 7 6 4 2 692 6 2 8 2 2 45 +820782447: 273 593 984 3 5 489 2 +6344588: 1 866 33 1 222 50 +244480: 1 59 6 86 8 816 45 66 +30431590286: 745 872 3 68 6 4 1 442 +1178358827: 530 4 7 554 827 +5472884572198: 380 5 3 71 85 5 36 94 1 +1290391: 1 17 308 11 2 +2279613269: 729 6 8 9 177 73 3 +2590613018: 29 91 47 433 4 1 5 +7528390101668: 17 8 939 47 83 744 71 +102796125: 6 8 5 1 3 454 961 2 5 +1199: 6 982 208 +108411: 9 1 6 98 8 7 12 9 71 36 4 +3589: 1 8 15 2 11 24 37 +16052: 60 6 2 98 3 45 9 1 3 71 6 +2795761: 706 396 1 +1307185: 197 7 4 6 8 17 1 9 9 1 55 +943: 920 3 20 +89520: 75 58 52 3 4 5 16 +9492769728: 6 325 2 234 564 8 1 3 9 +168032403: 1 89 3 3 465 27 77 17 +11444723221: 894 11 90 2 64 3 18 +3987348: 45 3 88 61 47 840 +399956: 99 897 74 4 72 +1724814019: 1 8 6 9 5 36 6 78 585 1 8 +484578732: 5 724 15 92 57 97 1 +120769: 9 99 55 2 74 +27834744915: 4 99 4 5 61 96 7 2 9 12 +1105: 852 6 247 +449470: 633 710 42 +7827644: 220 6 593 4 3 +53663904: 9 7 467 19 96 +55595: 2 7 3 538 95 +3050832153: 6 95 2 49 535 6 +8458892982058: 469 938 499 36 1 5 1 7 +9820815338177: 98 208 15 338 180 +86872304424: 45 288 6 11 93 2 14 2 2 +289200: 14 5 2 3 40 2 +4683023: 88 4 72 9 7 21 7 42 5 72 +1661: 9 2 788 127 652 +164507151418: 95 6 1 9 8 8 554 4 1 2 7 9 +23684882: 7 486 3 7 696 +93392: 1 9 3 348 47 +4472531682: 447 2 5 316 82 +13106: 6 26 97 127 28 4 1 69 +57641397: 12 32 131 13 98 +1175: 116 8 7 +167479498237: 3 13 62 656 6 6 863 +1436391: 53 5 92 4 8 75 8 8 3 +3078721: 8 3 91 16 5 4 87 321 9 +5580949230: 9 30 6 94 85 9 56 5 9 21 +12288573: 527 7 74 1 73 5 4 5 57 9 +2525017: 40 801 4 328 78 +153888: 43 5 49 9 730 96 +8869: 7 612 267 7 2 1 +60059346: 873 3 857 8 36 1 41 8 +896553742361: 3 838 7 2 5 3 302 7 363 +10924696802: 9 101 2 5 22 60 80 6 2 +15976400: 2 7 8 8 4 40 3 4 1 50 22 4 +930984: 860 9 5 5 3 28 7 48 8 3 +255420: 18 22 7 113 495 +604546: 529 593 87 5 43 +14895576: 7 8 5 7 420 9 8 9 3 762 +78330028659: 83 67 87 5 60 2 8 662 +2600107133221: 9 46 938 98 79 299 +4825632122: 8 668 2 602 1 75 47 +1360891: 765 747 9 81 8 +5967: 46 5 4 4 68 +920353480: 9 4 23 9 751 27 61 62 +6197067629: 9 7 195 560 1 878 585 +5203411241: 904 5 68 9 3 1 8 94 71 +170607390: 2 828 6 92 736 3 24 1 +5898240: 41 726 769 5 5 +1674650700: 83 7 414 89 505 +42447: 424 45 2 +59264679614: 9 6 5 264 67 9 537 77 +3475000: 3 38 2 7 3 1 999 +93749040857: 88 1 9 7 11 342 4 8 57 +3502961033286: 21 5 1 5 75 1 67 171 66 +87: 5 3 6 +435638: 7 2 4 2 804 1 518 +8507129374: 98 920 109 1 86 +4014337: 2 24 4 7 4 407 2 24 8 6 9 +762: 22 8 4 93 6 +5207537: 67 9 4 9 49 39 62 +494: 6 2 1 38 +287567280: 4 8 40 6 95 13 539 +861393517: 771 90 39 3 503 10 7 +101961: 3 748 45 6 973 +3330002: 490 7 67 97 4 +10592773: 80 63 996 93 73 +21000096: 56 800 72 78 6 +6955758: 3 3 83 49 674 1 7 1 +901589763: 3 424 914 651 7 3 12 8 +8136221: 738 68 7 3 2 28 92 2 1 +20737132707: 6 25 6 8 39 3 3 6 8 6 3 +16843229: 698 65 25 883 7 +5790: 90 7 8 3 9 944 2 3 9 3 +557: 53 1 26 +41: 5 7 6 +46052474: 2 7 74 11 54 31 72 5 9 +1232874: 24 3 344 60 5 35 1 1 +836: 3 3 5 57 11 198 +1318742655: 9 7 10 70 460 9 5 41 79 +2213400387345: 372 70 85 387 344 +447538349: 3 75 5 5 198 13 45 348 +421305: 1 4 21 30 2 +475727494328: 5 875 976 84 2 3 81 1 9 +522664: 9 87 40 63 8 94 62 5 4 +984484419769: 98 448 4 419 7 66 +28440766430: 623 70 456 46 432 +410257: 9 24 444 +6224579: 71 17 573 9 80 +2624: 1 8 5 1 64 +485102: 70 462 3 5 2 +3387672: 833 8 72 486 33 3 5 4 +13939339950: 72 6 8 4 233 25 6 +36188570398: 7 30 5 21 60 95 177 +9450: 86 17 428 5 1 +179686799: 57 574 9 8 5 568 2 +9857: 70 2 9 9 62 61 +101711: 2 7 6 6 3 8 668 45 +1514: 19 91 2 400 9 +49522: 4 144 332 11 378 +20507987520: 512 6 8 4 7 875 19 2 +49920136: 1 80 39 160 67 69 +15280764: 3 820 40 22 7 732 +17959201: 361 5 7 2 22 3 8 1 5 7 2 4 +297052: 1 6 5 457 4 +1066322: 73 49 8 23 67 324 +7987968737287: 496 8 79 553 80 8 8 9 7 +3725738880: 631 2 600 36 28 5 984 +33867960: 62 8 6 9 9 1 6 78 2 35 1 6 +12958044319: 719 882 1 9 3 18 1 9 19 +1369536: 1 1 430 3 6 25 266 3 7 +520: 7 88 9 5 +195279357: 38 3 6 2 6 82 9 5 9 56 +644193313: 4 8 3 6 8 140 7 4 4 22 5 +337: 9 216 7 99 5 +217374327: 96 3 1 6 9 4 37 3 419 57 +177168: 1 77 9 1 2 497 22 2 4 3 +511826: 945 3 2 80 89 76 +271368121049: 92 53 3 1 662 443 +121564804: 2 328 5 10 2 9 4 9 4 7 2 2 +72253: 7 2 123 87 45 +4948000293: 5 173 5 4 7 286 10 2 81 +8271: 7 24 8 5 83 592 9 1 39 9 +4885180322: 508 87 2 59 8 6 10 +597626214290: 8 25 464 58 751 9 41 +1153566: 8 393 742 10 565 +3488312704: 7 5 424 8 304 8 6 99 3 +576841707040: 795 205 403 18 40 +22511: 26 1 2 82 5 2 304 887 1 +13859: 529 924 526 7 4 +2969996810: 3 815 4 2 963 942 806 +128767: 1 6 4 8 6 8 20 8 5 1 56 +213042: 6 679 321 211 776 +54007: 744 2 9 4 433 6 +89929: 390 46 33 5 64 +11305189: 5 25 4 2 68 94 +3580256: 9 49 1 81 59 +4143480910: 31 15 5 85 88 903 7 +28323629: 2 1 1 7 92 224 8 362 9 +1408961706: 8 79 7 3 4 2 111 8 9 704 +126793: 939 1 15 2 66 652 213 +82831317: 3 380 8 7 245 +26737: 7 709 3 6 6 4 4 845 +82265531: 7 832 394 552 8 +455486995: 6 523 7 9 4 576 19 +2450459092: 2 9 9 4 7 5 2 607 10 9 1 1 +5222992320: 9 909 60 108 4 364 65 +140045802459: 273 7 5 45 802 433 26 +170593: 4 2 83 57 6 277 +18811455237: 9 4 2 1 145 3 1 4 9 74 1 8 +497002: 15 481 97 5 29 +11026974: 484 8 5 79 7 6 86 3 6 +2379942152: 83 706 94 78 284 +569: 81 352 6 81 49 +23630: 647 35 17 70 97 54 2 +7168521: 6 516 1 82 1 1 11 +41597309: 7 33 671 6 195 58 +51386: 50 81 5 571 +330445804866: 67 4 363 49 793 48 67 +797893: 71 3 84 839 52 +3952185: 98 99 4 60 653 86 +22660416505: 55 412 334 82 508 +11666: 33 333 2 596 70 9 +39908156: 1 28 5 14 717 98 6 +25878142856: 48 77 7 4 2 140 2 856 +83584512: 61 2 8 388 44 +511729: 730 5 2 52 7 +31059736924: 45 69 914 4 592 923 1 +129340224: 413 1 751 104 4 +499307: 68 9 81 8 33 7 9 +8757: 3 5 68 7 69 +5438484: 8 13 7 4 2 3 78 63 228 +13986: 9 226 190 31 7 804 +209498244: 571 69 10 6 2 443 +10560: 34 34 51 3 9 159 503 6 +529331671: 1 84 63 602 712 2 70 +7990488064: 896 80 891 62 2 +321138: 4 978 3 1 52 9 57 40 3 9 +3767895689: 6 28 95 51 759 599 +2315938: 321 6 4 8 5 239 12 6 2 8 +3125737510: 96 454 18 84 54 324 +10368172359241: 648 5 4 2 154 2 98 4 4 1 +445292: 4 1 1 29 5 9 7 207 42 8 +133644146231: 44 34 6 5 86 2 5 2 890 7 +560709349225: 5 8 7 95 83 82 16 4 166 +376533465: 92 9 491 5 371 +77130302295: 8 68 19 985 9 435 757 +20905: 6 9 3 101 1 +814649598: 9 6 307 52 1 4 2 15 1 63 +790078389: 89 1 79 790 78 387 +89129901: 89 6 6 7 3 2 3 255 661 +576: 2 9 14 319 5 +2104319: 9 470 2 6 9 5 81 2 2 7 9 +2904080: 3 2 19 6 2 9 2 9 481 6 85 +29893521: 8 46 9 7 5 3 3 582 9 5 4 6 +66945: 6 795 7 81 2 +255203011: 1 372 129 1 942 17 +2662327666: 7 664 67 230 89 1 662 +5357852124: 267 11 78 2 2 3 212 4 +103049: 2 3 4 55 8 87 1 +14755216: 53 9 3 14 981 52 13 3 +257122: 257 118 4 +10217900: 417 7 4 70 5 +136110748: 272 5 662 445 49 +3581: 71 280 6 9 +711: 59 18 489 69 76 +14580: 85 4 6 47 9 913 5 720 5 +12388730941: 9 3 2 6 9 8 389 5 1 846 2 +1322760556: 4 4 53 45 8 267 287 +1846678542: 13 9 8 7 1 1 66 785 44 +1091375766: 9 278 2 38 1 3 2 767 +733146778471: 6 2 8 9 8 9 2 751 1 1 97 6 +28737222081: 84 394 3 5 64 681 +76319806367: 780 5 9 9 9 797 4 9 1 96 +46274856: 4 1 804 572 59 +44101024: 228 47 6 271 43 16 +353939245524: 7 119 1 3 5 3 436 7 7 8 5 +22399091925: 297 4 2 5 852 5 3 5 6 44 +4086: 2 7 2 74 93 +536326: 4 355 49 903 29 +93362: 2 5 37 14 851 +11038817: 43 66 804 574 82 936 +44229148720: 22 5 1 572 813 58 80 4 +2982: 99 6 4 6 7 +122909973: 3 2 7 9 73 2 1 3 999 3 6 +772005: 527 1 8 481 3 +52734797555: 2 89 475 9 55 667 1 17 +596168243784: 3 16 1 9 2 69 82 43 781 +770: 7 5 684 9 2 +5956343: 7 46 41 34 2 45 51 +873108: 1 5 41 5 1 4 843 9 2 9 8 6 +617469: 86 1 40 7 7 62 8 +793460: 4 3 1 8 292 785 73 537 +146380: 5 28 4 183 664 300 +97948555854: 2 8 4 6 5 8 4 4 691 5 1 86 +76146205: 7 61 4 620 3 +235951: 983 4 4 1 6 +4100537553282: 64 94 4 1 8 7 213 3 2 85 +35482272: 51 5 397 3 532 +13291590297: 9 794 6 31 2 9 4 8 8 7 5 4 +1433303694: 73 9 7 512 6 5 32 493 +1580045514: 770 6 38 613 9 +7160046529: 800 9 745 1 2 6 10 86 2 +47470486: 21 55 218 5 5 20 5 6 8 7 +2262482802: 707 4 8 82 1 6 4 3 1 1 7 2 +702062: 195 9 4 29 35 +190725307756: 4 54 7 6 76 79 6 7 756 +82460: 303 2 22 9 3 +1518239: 983 830 832 574 9 +23055889402: 7 4 46 4 2 61 917 8 487 +354363961: 484 47 7 89 73 61 +2089734: 64 22 69 4 8 3 88 +3779961539: 99 98 16 8 487 +5458: 8 9 3 3 4 7 8 +407677166: 750 4 7 807 6 7 31 4 8 2 +39308163: 694 8 8 885 3 +126747155: 6 132 154 2 8 251 7 +20176891625: 806 276 59 87 907 +20085: 5 3 48 998 19 59 +418114382: 5 7 48 764 267 91 2 +2059684553: 752 9 77 370 95 +713077098: 99 8 3 3 2 32 7 7 3 3 80 7 +6594374: 22 637 43 7 3 +1562987934: 13 6 14 938 7 6 2 89 2 +101849060331: 1 3 5 4 1 457 8 1 2 25 9 9 +58979170: 5 230 584 6 91 2 8 15 8 +1495368078: 98 5 702 516 9 19 4 +511346649: 3 4 7 5 215 3 6 11 8 3 8 2 +235445568573139: 4 2 4 99 2 554 573 138 +7540930176: 6 9 310 8 16 2 68 1 +3906210: 23 79 43 3 890 +67364136525: 9 6 767 513 82 4 82 4 +510557215: 9 64 9 885 97 1 548 69 +341079556: 39 32 8 8 81 82 58 +38723838: 88 44 3 84 1 +8068: 42 43 4 844 +22133800: 6 153 1 9 3 343 45 8 +1315578: 2 6 1 74 5 3 280 6 6 6 4 3 +12239357: 3 770 152 8 26 12 220 +354910581706: 77 4 9 851 5 897 6 616 +454: 9 16 8 4 2 30 88 +1142: 4 72 567 101 +9885: 3 95 25 5 8 +28615342: 496 41 67 123 21 3 4 +1029787: 7 147 7 42 45 +13830: 7 6 9 740 2 2 1 124 3 39 +2413: 1 2 45 3 5 +19963791352: 3 9 30 4 3 2 92 1 927 1 2 +22316186966528: 32 199 688 833 832 +848710: 6 6 213 569 18 53 2 +5844: 7 7 347 978 8 +124818070161: 6 9 3 4 3 369 8 4 5 502 9 +1528188: 9 806 6 3 1 4 6 26 4 2 4 +38255: 42 3 852 1 3 +98612039402: 5 892 6 34 7 680 955 +28108634550: 593 60 5 500 79 49 +5749: 804 89 65 2 3 +5983513731: 318 294 220 96 86 8 8 +844265928: 882 676 1 2 354 2 +4754: 26 181 3 3 42 +403: 7 7 1 8 6 19 194 99 +20938590: 50 15 5 4 92 19 82 73 +223318: 5 5 6 8 9 4 6 5 774 972 9 +1996699: 2 5 786 221 247 1 692 +24795: 9 908 51 108 23 45 +598170468: 3 249 3 4 1 1 2 8 8 6 6 +8848672: 442 433 7 2 1 +315477: 4 9 9 73 186 995 60 +12122640: 20 9 93 94 3 720 +275664384: 36 2 8 6 2 8 102 4 153 +23372414: 72 380 8 35 9 4 64 +7008001: 936 7 92 838 6 8 4 +37585141: 375 35 49 61 53 2 +1514: 1 1 1 8 84 +5460480346: 6 28 4 80 6 18 346 +3541188: 843 2 5 1 838 2 8 621 7 +4949142: 72 3 29 7 79 26 +845: 5 92 7 4 374 +4422080: 51 161 293 8 65 +13000: 3 216 9 2 22 +16076280: 5 8 39 69 120 +25992255181724: 69 1 6 73 879 271 6 97 +42367599: 10 2 6 1 5 8 6 3 370 9 6 +377666: 327 613 4 4 59 7 +612575190949: 6 7 9 1 4 7 4 3 9 10 9 949 +215605: 53 3 5 77 8 +25732162: 4 975 68 78 2 97 +141265153: 397 93 71 50 3 +4832698: 6 2 64 864 700 +1799: 561 2 3 671 3 +614467: 176 451 980 8 +2469943: 382 7 869 5 8 2 8 7 49 +18191713494361: 9 8 642 3 769 571 35 4 +434950474717: 7 7 2 41 24 661 34 718 +45990013: 40 771 5 213 15 7 13 +4130365516679: 90 1 82 653 458 92 81 +37308684: 79 198 561 8 9 49 83 +1628: 9 41 4 81 70 +22675440: 5 327 7 6 5 5 66 4 72 4 6 +93431688605: 93 43 168 860 8 +16157: 55 4 484 15 590 +116564580: 9 1 3 5 5 8 9 5 3 99 5 68 +237636110: 410 644 9 67 43 +76969775: 6 3 3 580 2 72 713 +218447713: 2 8 78 45 2 714 +315041383: 28 6 7 69 4 3 2 196 37 +47840846537: 6 7 6 541 207 89 1 39 +988503654: 28 70 781 7 62 365 4 +116232654: 19 372 6 643 9 +111735066: 9 3 8 8 8 68 8 524 7 3 4 2 +1817575499: 861 823 3 2 26 415 1 +63390696446: 4 65 6 5 9 28 1 9 64 43 1 +738580012: 29 4 82 322 6 6 9 61 +1242147: 7 5 40 661 683 800 +43738589966: 4 373 85 8 90 8 5 2 9 1 4 +139972: 98 2 91 14 882 7 +154495527: 14 11 4 954 6 6 7 +51988: 1 518 2 6 6 +498078: 5 1 568 7 85 88 +8471: 3 172 5 17 43 +1098720: 41 71 7 5 117 3 3 88 90 +3220993: 590 954 66 2 991 +206545581: 6 20 78 6 55 89 4 9 8 4 +7841176: 4 36 196 699 476 +5646390883: 467 490 59 90 883 +3654642584: 9 396 90 58 6 256 2 5 +130942625902: 23 5 4 8 2 6 995 5 91 +22045656: 1 764 81 42 356 +627478410: 742 57 8 845 +603: 4 596 4 +4957495: 77 34 641 +13595839975496: 2 52 93 9 936 8 7 1 376 +761098063: 69 9 871 30 5 980 63 +131724535: 58 52 72 697 721 +3159: 9 6 6 9 4 601 385 5 7 1 +150715356: 2 6 66 4 6 5 43 328 6 6 6 +3652185: 3 71 68 7 7 9 4 86 6 361 +45630261: 9 994 38 73 51 +1259546885792: 419 136 4 2 1 477 63 +77263789755: 7 7 438 2 3 3 829 6 8 6 +6312980161295: 99 9 920 5 4 3 4 954 6 +874: 6 810 56 +206794512427: 1 2 5 1 4 7 3 4 20 1 402 5 +4241075177959: 29 914 71 9 86 4 8 2 8 4 +1268233188: 6 4 78 4 7 2 5 1 8 7 7 244 +254643: 3 391 646 49 67 +57552457531: 793 827 5 5 29 33 +243360: 45 766 20 3 +21575: 1 17 1 63 16 2 +4788137: 969 61 755 1 49 7 +551226080: 491 924 243 4 5 +54389104: 24 682 3 52 77 +174052800193: 6 8 8 1 4 5 9 4 6 34 4 196 +2029492: 3 245 7 547 5 47 74 +31953: 7 99 14 3 110 +15969771: 1 5 7 3 38 8 243 3 6 3 4 2 +6419328101: 8 5 118 2 640 68 1 23 8 +263669759: 9 3 86 768 672 8 3 24 4 +7732933: 721 3 3 7 1 85 47 1 3 6 7 +13049008058275: 4 16 9 626 7 6 5 582 7 5 +582119: 9 572 17 4 866 72 8 +267454667: 5 57 4 8 25 5 3 61 1 5 1 7 +6499467: 3 4 5 3 80 8 8 40 4 8 2 95 +78041738: 859 9 73 1 740 +1799761: 1 4 3 4 65 5 4 5 6 1 61 +581955071: 726 49 57 287 2 +430510057: 643 670 37 5 89 999 +3219088050: 208 28 1 31 23 5 5 762 +38129143: 7 2 37 8 1 92 5 2 36 4 6 +13920902: 46 301 67 35 4 97 7 +134581504: 4 94 979 357 1 +3215357: 2 782 8 425 354 +923: 27 890 2 4 +1211518464: 8 173 56 5 4 6 4 9 6 8 83 +12704960836: 2 16 1 252 1 486 7 4 35 +439941006: 47 8 8 2 7 1 20 5 2 33 4 9 +7777528: 8 903 4 85 28 +121083382: 4 74 764 8 72 3 17 +9958: 7 88 452 752 5 +34561803: 5 6 94 13 422 1 6 +10334766686: 492 7 922 3 6 87 +150289: 1 879 66 2 77 4 366 +580552: 579 531 2 28 989 +7410837: 340 7 562 8 38 +18540512: 3 610 1 6 267 7 8 6 7 3 2 +46119777: 28 854 581 10 9 +461179926912: 1 2 593 223 762 913 +5714938: 98 2 6 967 635 4 5 9 8 5 +5550: 26 909 5 794 81 +8394624803: 5 84 131 2 2 18 2 803 +1399: 5 2 290 90 4 3 205 9 3 1 +341160006: 2 5 4 34 737 138 7 40 +11205967: 87 447 916 42 2 184 +131378720351: 4 62 27 6 4 2 314 781 +16932035756: 7 6 330 4 354 2 7 9 1 76 +720: 14 1 6 71 629 +2282737137: 19 2 67 343 163 1 54 +2813767: 4 163 6 117 6 3 8 1 4 1 2 +3068463: 51 6 8 461 +1176192: 1 1 3 83 9 631 325 17 +136012: 8 889 17 5 4 37 +5879975453: 4 1 5 4 7 359 6 1 37 4 5 3 +491608106: 8 83 18 330 3 280 909 +1307268678: 2 32 81 25 2 9 221 457 +12671: 370 3 76 4 1 806 +8029550: 75 76 48 603 976 +1410706997550: 653 1 9 452 6 15 8 4 5 6 +3916639: 3 1 9 4 6 817 258 5 6 65 +11530851: 5 24 2 5 7 9 549 7 2 741 +115721: 96 5 10 1 533 938 2 +98385277: 4 4 53 476 909 276 +10133: 45 6 17 76 7 5 46 3 +41756: 7 5 69 7 50 6 +2144: 7 7 98 67 2 +1446603: 6 63 72 63 8 886 651 +139819152: 744 9 44 5 422 +3828: 335 275 8 4 4 3 5 6 7 8 9 +129353136666: 41 478 96 66 631 36 +3438094: 343 269 253 281 8 53 +7326: 18 9 15 2 18 +15617004: 22 2 9 302 679 7 +30917: 2 593 26 76 8 +6092883: 7 2 7 6 9 1 34 1 64 7 6 8 +551603000: 52 9 5 8 6 543 6 1 650 4 +123918: 9 1 2 644 9 6 +39657: 77 2 7 25 982 +124840: 12 41 8 4 87 1 68 503 +179966207842: 85 8 9 6 51 97 4 19 6 4 5 +718854: 46 9 3 6 664 8 86 63 +926545746: 9 265 457 4 6 +674908382899: 41 1 66 9 9 1 6 3 289 9 +1755015393918: 540 5 13 3 5 3 8 7 6 9 1 8 +107210: 7 7 93 189 20 +40936: 1 297 84 3 21 5 8 2 2 1 7 +35354: 3 21 87 1 11 673 5 +1576785524: 5 6 6 2 7 8 21 5 8 9 3 521 +43218: 432 11 4 6 +3322: 8 21 4 32 3 +440344195: 8 9 788 8 17 8 3 81 53 5 +1128: 8 3 33 24 72 +36615177: 91 574 73 40 7 +195: 9 8 66 51 6 +141600555813: 14 39 5 6 90 82 9 5 4 6 7 +1181669: 980 13 17 7 2 +761996: 76 15 4 9 6 +976585475: 673 517 95 79 82 +3548311955: 51 694 89 11 952 +9732638: 8 9 541 34 513 +45902666880: 5 70 3 8 7 759 7 8 515 9 +125069814644: 6 461 461 822 5 8 943 +710696: 53 59 785 917 8 +465599: 7 2 919 4 19 8 5 5 2 1 +259967957955: 9 8 647 814 5 41 50 5 +3845565568: 881 5 873 56 2 4 1 +190592649449: 7 37 38 8 8 8 649 447 +756057: 4 461 4 3 5 56 46 2 34 +111361216: 28 5 2 5 2 68 5 9 5 1 46 +110088843: 27 5 14 8 4 840 +4387653: 39 71 8 367 570 52 1 +2305381: 2 305 3 82 +15439: 765 6 23 41 2 +46691409877612: 58 3 6 42 59 3 347 8 14 +10402885471: 30 448 42 516 26 3 5 +367703352437: 664 879 45 5 7 1 2 434 +432397019: 661 59 6 617 60 +161713440: 156 799 97 180 7 122 +200727: 9 52 206 50 32 5 3 +4664366888: 466 1 436 68 85 +4225790: 7 25 86 222 5 874 +313967: 5 1 19 64 3 4 51 11 +25424010: 5 5 376 48 10 13 +869235228: 285 1 105 7 5 74 391 1 +6422495: 6 9 90 88 1 695 +231217: 61 78 176 6 338 74 6 4 +175: 3 54 13 +356649: 51 873 57 8 9 +20662785: 76 941 982 1 2 285 +668781667: 723 8 959 954 +213680962: 18 6 89 8 5 2 3 94 1 570 +409: 9 57 83 6 7 244 +9100212539: 68 941 66 2 539 +4425754: 1 7 85 37 5 43 635 11 +7446: 95 5 7 666 94 1 +24189389894: 7 3 3 2 87 3 55 8 446 96 +73966855: 6 66 257 6 18 4 74 1 3 7 +2754514531958: 459 6 514 5 31 958 2 1 +2461425673: 659 498 48 4 933 +5055058199: 2 9 9 29 82 10 7 7 7 23 +9960104: 322 50 69 14 44 +1154: 60 9 92 473 8 41 +76056154128: 9 4 1 216 8 21 87 412 8 +1690148990: 1 640 7 3 9 8 6 3 50 95 2 +170115: 67 4 85 1 9 30 464 17 6 +853869660: 1 9 4 87 30 6 8 9 4 4 8 8 +6792: 81 71 2 8 9 95 574 9 8 +6716711692856: 6 9 482 748 6 8 58 855 +41172790: 1 40 524 73 790 +17788396: 37 5 2 21 38 2 2 6 4 27 4 +190790305: 8 293 365 223 426 +1193801: 4 4 651 611 940 +2422522291: 346 7 520 2 283 8 +13308: 64 957 5 83 12 +4293863484: 612 818 9 84 953 +149017: 5 98 660 4 43 33 9 7 3 4 +72: 9 7 2 38 13 +226993231: 97 10 26 60 70 17 90 +11072: 5 8 19 5 1 +4978271: 71 7 8 269 5 +992998: 3 22 4 7 3 44 1 9 12 368 +313930837: 313 92 8 2 835 +102854: 1 399 5 5 51 7 849 1 +409530: 549 36 7 21 9 +792211: 4 3 922 1 1 +160645: 7 63 394 3 55 31 9 7 19 +1951: 4 33 4 7 120 7 4 3 1 8 2 8 +38349229: 18 5 5 9 22 4 4 7 247 7 2 +24060: 3 8 4 8 92 113 104 1 +239912: 655 214 276 60 8 +2823624050: 98 9 3 8 4 8 41 763 8 +61741: 686 9 1 +12031786809: 911 1 3 1 99 6 4 11 5 3 8 +34865617: 899 524 70 74 814 +1249568: 15 76 259 3 895 544 +1047251: 70 15 5 992 691 +162436904: 203 4 1 8 426 2 1 42 8 3 +2284918477: 923 643 3 4 521 7 3 6 +12166392: 423 8 84 1 4 8 14 84 +303534248989: 6 72 629 6 806 9 8 386 +2099524667214: 72 90 9 4 581 2 402 8 +21542441041: 8 9 7 8 561 2 200 9 7 7 3 +6383320: 68 561 97 836 1 8 +19518067475: 49 77 21 61 8 848 +398142456: 8 2 6 241 1 4 6 6 15 4 9 2 +4538716967: 74 9 6 58 2 6 1 9 91 965 +216597935617: 30 313 806 243 4 63 5 +74528982: 431 3 9 8 6 3 4 4 8 74 2 +26089807308: 5 207 2 9 252 499 3 10 +2094995: 3 4 314 397 503 +347304287: 5 988 58 285 +134526480: 13 93 63 7 7 5 4 1 60 44 +47110131597: 673 70 131 59 7 +33282630423: 528 295 715 6 63 +63099288: 1 4 1 15 4 58 1 3 4 1 84 7 +18351027: 25 810 5 302 3 +15327: 3 509 29 402 77 +718477: 79 8 3 9 8 +156918: 6 4 53 296 33 5 +13721761037: 163 741 7 4 4 9 5 6 284 +3725196162: 73 63 81 610 5 54 +84577739: 4 88 47 2 4 815 3 299 +185618730: 1 4 4 925 79 1 3 1 8 7 +1163675588: 4 3 7 14 8 1 4 84 31 9 7 5 +379019877: 80 30 5 4 944 98 +228721777: 9 29 1 6 9 2 1 9 1 2 984 5 +6294235021: 86 9 7 6 9 67 1 9 4 18 3 3 +158746596: 90 467 95 5 32 3 +18697431689: 41 57 4 7 15 84 2 9 +858844: 483 886 725 759 2 +55947: 6 782 71 +28295433: 19 541 362 17 4 +1998070830: 8 476 5 583 72 4 6 45 +5046835618: 787 27 62 26 9 618 +1639: 1 50 7 3 2 6 7 665 9 9 2 7 +7414: 13 34 776 9 8 +12135987: 720 36 52 7 6 590 9 +3592173389074: 98 61 8 9 55 64 846 6 +148153: 16 5 3 85 21 +19227110478: 6 6 7 608 6 4 4 6 50 71 4 +1261831906: 98 58 2 64 7 908 +8451760895: 4 2 4 9 7 879 707 89 8 +37032: 2 2 2 96 635 2 1 2 1 47 6 +24846: 7 273 1 573 4 +100699: 37 214 401 28 1 14 5 +774056700558: 1 767 252 6 47 94 73 6 +4290567: 5 297 810 +30499049925897: 11 9 9 9 6 8 85 305 896 +6607390: 277 6 238 9 30 473 +161281: 72 28 8 1 1 +214267392: 66 9 585 6 8 4 +3746933: 61 7 70 6 753 5 1 +13362239: 90 432 825 992 1 +355: 3 3 39 4 +3528: 21 16 56 9 +48083163: 8 290 2 580 1 +125953633: 8 231 527 6 35 +49621348: 492 74 1 346 3 48 +328566351: 84 628 2 9 11 233 2 2 +897549442: 997 277 9 54 88 +1518061: 758 2 8 2 62 +11728: 2 1 36 770 582 75 8 +440725865: 8 6 35 5 314 5 54 3 1 6 7 +17245369: 6 303 70 1 70 7 1 88 9 +1128025843: 8 2 736 30 5 5 798 46 +1387547: 6 330 7 17 543 987 +1735611512: 779 88 805 753 2 1 5 +59018886: 83 4 9 7 6 99 8 83 97 1 9 +14972265: 8 56 23 763 77 87 +98394814: 9 26 7 9 861 9 1 3 1 4 +62171: 8 54 987 975 2 +747726641: 392 89 25 987 1 9 8 7 +193535: 4 9 96 8 7 +8491504: 925 1 925 459 4 +19943778: 11 1 41 335 121 3 555 +1233154550: 89 517 335 7 762 8 +104363998: 88 61 7 639 98 +1211277625: 68 4 95 330 9 4 4 7 62 5 +22727771: 250 4 504 638 18 +272163135216: 704 355 94 4 23 42 +21950784286: 7 6 9 3 3 270 4 48 2 2 6 6 +472: 2 95 5 85 90 67 125 3 +1915: 5 65 727 108 755 +16669773: 7 984 623 6 9 3 +105548118179: 387 439 345 9 62 182 +6718: 3 1 3 11 43 56 +84500240: 989 89 192 5 80 +20729408: 4 1 182 32 1 4 127 +2059431192: 5 1 9 324 9 1 376 3 8 9 7 +6661244: 239 306 61 91 501 +912840: 77 40 195 6 40 +8462797: 82 5 905 7 8 528 6 3 8 6 +518913992: 3 5 1 4 1 25 6 4 6 615 9 2 +53096: 9 8 726 824 +3277428146350: 3 27 74 207 74 463 4 8 +688291: 593 829 484 33 11 +3394: 4 6 9 36 52 4 +1035101750: 419 73 43 9 787 +21467269505: 4 95 5 76 388 3 4 8 3 3 7 +13893516625: 28 199 248 222 8 3 22 +19972600: 9 29 56 63 1 597 +6298749: 1 3 9 2 8 282 3 2 8 461 9 +22: 1 8 1 1 13 +9070658525: 3 1 82 8 82 3 64 9 924 8 +14490586: 2 4 30 84 53 9 4 95 2 6 3 +706408: 4 53 12 22 411 +57200: 7 679 28 1 1 5 16 +1940489988: 4 4 7 3 5 6 7 22 899 88 +317776438: 642 707 72 34 7 +91211400641: 900 95 4 381 74 16 2 7 +186: 171 8 7 +11660572695: 3 721 7 73 17 317 +12904914: 3 6 7 1 7 3 1 4 9 7 93 141 +21267013269: 365 2 35 30 1 58 59 94 +1730625: 949 777 4 625 +36784: 98 5 75 31 3 +145262: 309 6 1 830 37 +888036: 2 9 9 4 8 311 12 8 3 4 9 5 +107616111478: 442 11 34 927 651 +292000: 81 5 6 7 1 6 1 980 8 4 4 +3765: 2 787 420 46 3 +46121893264: 9 4 9 90 9 45 6 6 1 2 4 40 +5795330413: 81 4 2 9 607 25 60 9 6 4 +36648062: 2 4 144 45 62 +6836468: 683 55 8 76 94 +1436309: 64 9 4 783 310 +2196829: 540 8 5 1 2 58 3 811 2 +11799356411: 4 9 5 4 7 8 156 558 9 5 +1971634891: 3 7 123 2 21 6 4 7 2 2 4 3 +9397275528: 12 791 1 9 7 5 99 +274978673783: 55 862 11 617 58 +13841: 3 4 4 78 165 +120688822: 15 62 83 805 148 2 +225387750: 4 4 3 1 9 3 5 5 8 875 6 7 +393785: 3 880 55 37 180 67 +1762656: 65 6 2 439 5 24 +220788852: 29 2 54 7 88 51 +43526370: 4 765 16 889 930 +737100420299: 9 9 70 975 7 6 4 5 60 5 +157961882: 3 2 987 8 76 959 7 5 4 5 +1802: 7 5 2 21 8 93 720 +7346461: 6 898 394 54 461 +8503740: 9 5 2 92 85 7 +5919855997: 8 2 333 7 584 855 998 +390937659: 92 7 8 9 667 2 5 1 25 6 3 +365744: 516 29 3 822 4 8 +8057514: 7 5 45 5 7 5 16 +28017586: 8 98 7 5 95 8 30 95 8 7 +202759256: 4 134 40 70 9 9 6 +471150: 39 470 469 4 43 25 +513685: 2 8 7 5 61 220 71 +1543834258722: 380 736 6 71 5 1 92 5 +3384988870: 5 8 4 4 1 3 8 6 721 5 2 4 +4764: 35 4 5 5 3 7 7 59 1 1 74 4 +1184974: 28 19 42 8 6 53 +4000143113: 35 8 703 238 556 +418: 4 1 7 2 +9557037: 61 1 4 391 3 992 +105661643: 573 5 4 94 49 +23214254: 382 4 9 6 252 +5444: 46 4 4 3 4 7 +737349: 94 5 32 8 49 +5122916: 2 48 71 51 919 +5821000: 79 736 63 2 6 5 3 +8301678: 77 18 155 73 561 +1501597072: 7 6 4 9 787 265 102 5 5 +718052598: 491 7 6 4 4 8 4 4 454 9 +178497: 4 1 6 11 6 413 52 4 99 3 +540: 7 9 10 4 1 18 +9103497120: 9 400 713 1 97 2 680 9 +5630913090: 782 8 570 1 90 +1054463962: 43 4 6 361 56 4 95 9 63 +20116902: 8 244 15 7 721 4 74 2 +898710: 70 26 102 126 581 +14793708485071: 959 6 539 3 91 9 530 +50330139441: 99 270 49 2 507 +6807041: 41 527 3 5 21 836 +1031: 76 27 1 +93138: 9 218 95 8 +258123: 57 44 4 658 5 700 +46365920: 89 374 3 2 3 7 5 3 6 1 7 1 +11859124: 2 5 28 798 55 8 2 6 90 2 +23250824: 539 32 674 4 1 8 2 496 +545806394: 3 7 8 1 5 484 6 8 1 9 6 49 +10832: 185 8 21 23 4 7 53 7 76 +100339266: 8 67 26 72 64 +7412898: 92 66 8 91 7 +257523634800: 9 566 32 964 9 815 60 +185426200: 1 8 5 4 261 4 3 50 1 1 9 +10433434471: 92 177 451 86 468 +216571: 7 8 6 865 734 6 9 +1104: 2 52 6 2 1 +5318188: 23 2 317 671 513 +1007609179: 34 7 1 45 141 3 29 +323629: 7 1 5 8 1 378 9 +3521036: 6 698 5 963 2 69 +95660213: 1 7 948 98 2 620 13 +64638: 6 46 37 1 +17385402: 173 84 48 7 904 3 3 8 +1616421504: 5 498 7 6 7 1 9 2 3 7 5 3 +715025: 71 502 5 +20047048: 1 260 158 488 8 +216208384: 802 4 62 556 448 +4235581375: 4 7 9 451 8 92 7 137 4 2 +8823357451: 80 64 74 328 71 11 +3406644959: 8 8 9 6 2 643 7 324 4 95 +3356625: 670 433 892 5 +516787406: 2 33 832 55 9 9 4 5 4 1 6 +14601: 21 1 7 6 3 975 +39390: 508 39 72 6 1 +57737790013: 60 1 911 5 6 5 4 495 13 +75067696812: 21 5 88 1 8 869 4 812 +28721: 2 3 5 68 5 33 +299147159: 19 72 7 9 61 925 7 308 +47958: 6 290 162 2 4 +4810465342: 87 463 55 335 5 +1084622973: 2 8 8 4 6 2 3 5 7 963 5 2 +940412473: 7 23 54 540 3 3 6 27 4 2 +1251718: 6 69 2 25 72 9 1 9 252 3 +2276640: 4 9 5 86 93 2 9 6 31 6 8 +363950: 908 5 8 5 553 +963651: 6 4 938 980 663 8 +4385541040633: 515 946 85 40 633 +1498665: 4 6 23 174 87 3 +4678232686: 8 584 2 60 3 268 1 7 +682407: 6 822 74 122 13 +3718652394: 6 1 53 86 5 2 391 +22789229870: 5 426 42 298 70 +128646776484: 4 4 2 7 8 699 2 4 18 49 +52417206331: 6 798 6 771 331 +3040286: 538 593 1 389 1 4 2 6 +198047: 9 45 27 9 2 499 713 1 5 +21645166: 7 1 6 7 91 1 276 2 2 347 +22206000: 6 6 72 3 95 731 749 6 5 +1402633431: 253 924 2 39 6 +24223828: 928 8 647 3 7 4 +5140898383: 72 714 9 83 1 8 3 +11209432: 70 8 2 70 24 33 1 +5551629375: 9 7 9 7 782 7 5 5 9 8 612 +119196: 80 70 29 3 1 7 299 1 1 3 +15894764944: 595 667 504 4 494 2 +1285987: 47 342 7 1 8 +9717593: 94 28 289 59 5 +263: 5 41 52 6 +153682480: 630 9 497 89 3 56 5 +1937661: 8 157 993 4 86 2 79 +84669705: 851 449 5 801 81 +875552: 8 751 99 50 303 +96459: 160 1 7 6 6 +562626: 92 95 54 3 6 +19640: 24 55 8 2 +67784330: 4 4 71 36 95 5 8 2 2 2 8 7 +213868: 25 4 842 +105733: 4 15 30 28 54 diff --git a/src/holt59/aoc/inputs/holt59/2024/day8.txt b/src/holt59/aoc/inputs/holt59/2024/day8.txt index e69de29..1e7f2b1 100644 --- a/src/holt59/aoc/inputs/holt59/2024/day8.txt +++ b/src/holt59/aoc/inputs/holt59/2024/day8.txt @@ -0,0 +1,50 @@ +.........................p........................ +......................h....C............M......... +..............................p....U.............. +..5..................p............................ +..6z...........................................C.. +...............c...........zV..................... +...5.....c........................................ +.Z.............h........S...z....9................ +.O............................9...z........M..C... +..O....5..............................F..M..C..... +..Z.........5.c...............M....V.............. +........ZO................q....................... +s...O................h..Uq.....7V...........4..... +.q.g..............c.............p.......4......... +............hZ.............................4G..... +6s...........................U.Q.....3............ +.......6.................9.......Q.............3.. +....s..D.........................6................ +.............................................FL... +.................................................. +..g...D.........q.....f.......Q...F....L......7... +...............2.........f.............V.L...4.... +...................2.s...................f3......G +....g...........................v......7P......... +..2..g.............d.....v...........P.......1.... +..............u.........f.............L........G.. +.........l.D....u...............d........o..P..... +..................8...............9..1......o...7. +............l..................................... +...................l...S...........F.......o..U... +.......................u...S...................... +..........l....u...............m...........P....G. +......................................1.8.......o. +.................................................. +..................v.......S................0...... +.............v........d.....1..................... +.................................................. +..........D....................................0.. +...................m.............H..........0..... +...................................d......0....... +.................................................. +....Q............................................. +................................H................. +........................H....................8.... +.................................................. +.................................................. +.........................................8........ +.......................H3......................... +............................m..................... +................................m................. diff --git a/src/holt59/aoc/inputs/tests/2024/day7.txt b/src/holt59/aoc/inputs/tests/2024/day7.txt index e69de29..fc6e099 100644 --- a/src/holt59/aoc/inputs/tests/2024/day7.txt +++ b/src/holt59/aoc/inputs/tests/2024/day7.txt @@ -0,0 +1,9 @@ +190: 10 19 +3267: 81 40 27 +83: 17 5 +156: 15 6 +7290: 6 8 6 15 +161011: 16 10 13 +192: 17 8 14 +21037: 9 7 18 13 +292: 11 6 16 20 diff --git a/src/holt59/aoc/inputs/tests/2024/day8.txt b/src/holt59/aoc/inputs/tests/2024/day8.txt index e69de29..78a1e91 100644 --- a/src/holt59/aoc/inputs/tests/2024/day8.txt +++ b/src/holt59/aoc/inputs/tests/2024/day8.txt @@ -0,0 +1,12 @@ +............ +........0... +.....0...... +.......0.... +....0....... +......A..... +............ +............ +........A... +.........A.. +............ +............