From 664dcfe7ba7773227b53f75adabc095f9cf561fd Mon Sep 17 00:00:00 2001 From: Mikael CAPELLE Date: Wed, 4 Dec 2024 17:09:24 +0100 Subject: [PATCH] Refactor 2023 for new system. --- poetry.lock | 16 +- pyproject.toml | 1 + 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/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 | 30 ++- 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/__main__.py | 2 +- src/holt59/aoc/base.py | 6 +- 24 files changed, 1119 insertions(+), 1085 deletions(-) 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/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/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..d75d1b7 100644 --- a/src/holt59/aoc/2023/day25.py +++ b/src/holt59/aoc/2023/day25.py @@ -1,25 +1,23 @@ -import sys +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/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/__main__.py b/src/holt59/aoc/__main__.py index 59b0b11..a052fb8 100644 --- a/src/holt59/aoc/__main__.py +++ b/src/holt59/aoc/__main__.py @@ -54,7 +54,7 @@ def main(): f".{year}.day{day}", __package__ ).Solver - solver = solver_class(logging.getLogger("AOC"), year, day) + solver = solver_class(logging.getLogger("AOC"), verbose, year, day) data: str if stdin: diff --git a/src/holt59/aoc/base.py b/src/holt59/aoc/base.py index 9ff1a16..45893d2 100644 --- a/src/holt59/aoc/base.py +++ b/src/holt59/aoc/base.py @@ -4,10 +4,14 @@ from typing import Any, Final, Iterator class BaseSolver: - def __init__(self, logger: Logger, year: int, day: int): + def __init__( + self, logger: Logger, verbose: bool, year: int, day: int, outputs: bool = False + ): self.logger: Final = logger + self.verbose: Final = verbose self.year: Final = year self.day: Final = day + self.outputs = outputs @abstractmethod def solve(self, input: str) -> Iterator[Any]: ...