Refactor 2023 for new system.

This commit is contained in:
Mikael CAPELLE 2024-12-04 17:09:24 +01:00
parent a9bcf9ef8f
commit 664dcfe7ba
24 changed files with 1119 additions and 1085 deletions

16
poetry.lock generated
View File

@ -1245,6 +1245,20 @@ files = [
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] 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"] 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]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.12.2" version = "4.12.2"
@ -1281,4 +1295,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "b643261f91a781d77735e05f6d2ac1002867600c2df6393a9d1a15f5e1189109" content-hash = "c91bc307ff4a5b3e8cd1976ebea211c9749fe09d563dd80861f70ce26826cda9"

View File

@ -23,6 +23,7 @@ ruff = "^0.8.1"
poethepoet = "^0.31.1" poethepoet = "^0.31.1"
ipykernel = "^6.29.5" ipykernel = "^6.29.5"
networkx-stubs = "^0.0.1" networkx-stubs = "^0.0.1"
types-networkx = "^3.4.2.20241115"
[tool.poetry.scripts] [tool.poetry.scripts]
holt59-aoc = "holt59.aoc.__main__:main" holt59-aoc = "holt59.aoc.__main__:main"

View File

@ -1,37 +1,38 @@
import os from typing import Any, Iterator, Literal, cast
import sys
from typing import Literal, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
Symbol = Literal["|", "-", "L", "J", "7", "F", ".", "S"] 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 class Solver(BaseSolver):
si, sj = next( def solve(self, input: str) -> Iterator[Any]:
lines: list[list[Symbol]] = [
[cast(Symbol, symbol) for symbol in line] for line in input.splitlines()
]
# find starting point
si, sj = next(
(i, j) (i, j)
for i in range(len(lines)) for i in range(len(lines))
for j in range(len(lines[0])) for j in range(len(lines[0]))
if lines[i][j] == "S" if lines[i][j] == "S"
) )
# find one of the two outputs # find one of the two outputs
ni, nj = si, sj ni, nj = si, sj
for ni, nj, chars in ( for ni, nj, chars in (
(si - 1, sj, "|7F"), (si - 1, sj, "|7F"),
(si + 1, sj, "|LJ"), (si + 1, sj, "|LJ"),
(si, sj - 1, "-LF"), (si, sj - 1, "-LF"),
(si, sj + 1, "-J7"), (si, sj + 1, "-J7"),
): ):
if lines[ni][nj] in chars: if lines[ni][nj] in chars:
break break
# part 1 - find the loop (re-used in part 2) # part 1 - find the loop (re-used in part 2)
loop = [(si, sj), (ni, nj)] loop = [(si, sj), (ni, nj)]
while True: while True:
pi, pj = loop[-2] pi, pj = loop[-2]
i, j = loop[-1] i, j = loop[-1]
@ -51,30 +52,29 @@ while True:
loop.append((i, j)) loop.append((i, j))
answer_1 = len(loop) // 2 yield len(loop) // 2
print(f"answer 1 is {answer_1}")
# part 2 # part 2
# replace S by an appropriate character for the loop below # replace S by an appropriate character for the loop below
di1, dj1 = loop[1][0] - loop[0][0], loop[1][1] - loop[0][1] 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] di2, dj2 = loop[0][0] - loop[-1][0], loop[0][1] - loop[-1][1]
mapping: dict[tuple[int, int], dict[tuple[int, int], Symbol]] = { 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): "F", (1, 0): "L"},
(0, -1): {(0, -1): "-", (-1, 0): "7", (1, 0): "J"}, (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): "7", (0, -1): "F"},
(-1, 0): {(-1, 0): "|", (0, -1): "L", (0, 1): "J"}, (-1, 0): {(-1, 0): "|", (0, -1): "L", (0, 1): "J"},
} }
lines[si][sj] = mapping[di1, dj1][di2, dj2] lines[si][sj] = mapping[di1, dj1][di2, dj2]
# find the points inside the loop using an adaptation of ray casting for a discrete # find the points inside the loop using an adaptation of ray casting for a discrete
# grid (https://stackoverflow.com/a/218081/2666289) # grid (https://stackoverflow.com/a/218081/2666289)
# #
# use a set for faster '... in loop' check # use a set for faster '... in loop' check
# #
loop_s = set(loop) loop_s = set(loop)
inside: set[tuple[int, int]] = set() inside: set[tuple[int, int]] = set()
for i in range(len(lines)): for i in range(len(lines)):
cnt = 0 cnt = 0
for j in range(len(lines[0])): for j in range(len(lines[0])):
if (i, j) not in loop_s and cnt % 2 == 1: if (i, j) not in loop_s and cnt % 2 == 1:
@ -83,18 +83,18 @@ for i in range(len(lines)):
if (i, j) in loop_s and lines[i][j] in "|LJ": if (i, j) in loop_s and lines[i][j] in "|LJ":
cnt += 1 cnt += 1
if VERBOSE: if self.verbose:
for i in range(len(lines)): for i in range(len(lines)):
s = ""
for j in range(len(lines[0])): for j in range(len(lines[0])):
if (i, j) == (si, sj): if (i, j) == (si, sj):
print("\033[91mS\033[0m", end="") s += "\033[91mS\033[0m"
elif (i, j) in loop: elif (i, j) in loop:
print(lines[i][j], end="") s += lines[i][j]
elif (i, j) in inside: elif (i, j) in inside:
print("\033[92mI\033[0m", end="") s += "\033[92mI\033[0m"
else: else:
print(".", end="") s += "."
print() self.logger.info(s)
answer_2 = len(inside) yield len(inside)
print(f"answer 2 is {answer_2}")

View File

@ -1,18 +1,22 @@
import sys from typing import Any, Iterator
import numpy as np import numpy as np
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
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
def compute_total_distance(expansion: int) -> int: class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.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
def compute_total_distance(expansion: int) -> int:
distances: list[int] = [] distances: list[int] = []
for g1 in range(len(galaxies_y)): for g1 in range(len(galaxies_y)):
x1, y1 = int(galaxies_x[g1]), int(galaxies_y[g1]) x1, y1 = int(galaxies_x[g1]), int(galaxies_y[g1])
@ -31,11 +35,8 @@ def compute_total_distance(expansion: int) -> int:
distances.append(dx + dy) distances.append(dx + dy)
return sum(distances) return sum(distances)
# part 1
yield compute_total_distance(2)
# part 1 # part 2
answer_1 = compute_total_distance(2) yield compute_total_distance(1000000)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = compute_total_distance(1000000)
print(f"answer 2 is {answer_2}")

View File

@ -1,9 +1,7 @@
import os
import sys
from functools import lru_cache 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 @lru_cache
@ -77,31 +75,29 @@ def compute_possible_arrangements(
) )
def compute_all_possible_arrangements(lines: Iterable[str], repeat: int) -> int: class Solver(BaseSolver):
def compute_all_possible_arrangements(
self, lines: Iterable[str], repeat: int
) -> int:
count = 0 count = 0
if VERBOSE: for i_line, line in enumerate(lines):
from tqdm import tqdm self.logger.info(f"processing line {i_line}: {line}...")
lines = tqdm(lines)
for line in lines:
parts = line.split(" ") parts = line.split(" ")
count += compute_possible_arrangements( count += compute_possible_arrangements(
tuple(filter(len, "?".join(parts[0] for _ in range(repeat)).split("."))), tuple(
filter(len, "?".join(parts[0] for _ in range(repeat)).split("."))
),
tuple(int(c) for c in parts[1].split(",")) * repeat, tuple(int(c) for c in parts[1].split(",")) * repeat,
) )
return count return count
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
lines = sys.stdin.read().splitlines() # part 1
yield self.compute_all_possible_arrangements(lines, 1)
# part 2
# part 1 yield self.compute_all_possible_arrangements(lines, 5)
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}")

View File

@ -1,5 +1,6 @@
import sys from typing import Any, Callable, Iterator, Literal
from typing import Callable, Literal
from ..base import BaseSolver
def split(block: list[str], axis: Literal[0, 1], count: int) -> int: 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 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
# part 1 yield sum(
answer_1 = sum(
split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0) split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0)
for block in blocks for block in blocks
) )
print(f"answer 1 is {answer_1}")
# part 2 # part 2
answer_2 = sum( yield sum(
split(block, axis=1, count=1) + 100 * split(block, axis=0, count=1) split(block, axis=1, count=1) + 100 * split(block, axis=0, count=1)
for block in blocks for block in blocks
) )
print(f"answer 2 is {answer_2}")

View File

@ -1,10 +1,9 @@
import sys from typing import Any, Iterator, TypeAlias
from typing import TypeAlias
from ..base import BaseSolver
RockGrid: TypeAlias = list[list[str]] RockGrid: TypeAlias = list[list[str]]
rocks0 = [list(line) for line in sys.stdin.read().splitlines()]
def slide_rocks_top(rocks: RockGrid) -> RockGrid: def slide_rocks_top(rocks: RockGrid) -> RockGrid:
top = [0 if c == "." else 1 for c in rocks[0]] top = [0 if c == "." else 1 for c in rocks[0]]
@ -34,21 +33,25 @@ def cycle(rocks: RockGrid) -> RockGrid:
return rocks 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 rocks = slide_rocks_top([[c for c in r] for r in rocks0])
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}")
# part 2 # part 1
rocks = rocks0 yield sum(
(len(rocks) - i) * sum(1 for c in row if c == "O")
for i, row in enumerate(rocks)
)
N = 1000000000 # part 2
cycles: list[RockGrid] = [] rocks = rocks0
i_cycle: int = -1
for i_cycle in range(N): N = 1000000000
cycles: list[RockGrid] = []
i_cycle: int = -1
for i_cycle in range(N):
rocks = cycle(rocks) rocks = cycle(rocks)
if any(rocks == c for c in cycles): if any(rocks == c for c in cycles):
@ -56,13 +59,12 @@ for i_cycle in range(N):
cycles.append([[c for c in r] for r in rocks]) cycles.append([[c for c in r] for r in rocks])
cycle_start = next(i for i in range(len(cycles)) if (rocks == cycles[i])) cycle_start = next(i for i in range(len(cycles)) if (rocks == cycles[i]))
cycle_length = i_cycle - cycle_start cycle_length = i_cycle - cycle_start
ci = cycle_start + (N - cycle_start) % cycle_length - 1 ci = cycle_start + (N - cycle_start) % cycle_length - 1
answer_2 = sum( yield sum(
(len(rocks) - i) * sum(1 for c in row if c == "O") (len(rocks) - i) * sum(1 for c in row if c == "O")
for i, row in enumerate(cycles[ci]) for i, row in enumerate(cycles[ci])
) )
print(f"answer 2 is {answer_2}")

View File

@ -1,21 +1,24 @@
import sys
from functools import reduce from functools import reduce
from typing import Any, Iterator
steps = sys.stdin.read().strip().split(",") from ..base import BaseSolver
def _hash(s: str) -> int: def _hash(s: str) -> int:
return reduce(lambda v, u: ((v + ord(u)) * 17) % 256, s, 0) return reduce(lambda v, u: ((v + ord(u)) * 17) % 256, s, 0)
# part 1 class Solver(BaseSolver):
answer_1 = sum(map(_hash, steps)) def solve(self, input: str) -> Iterator[Any]:
print(f"answer 1 is {answer_1}") steps = input.split(",")
# part 2 # part 1
boxes: list[dict[str, int]] = [{} for _ in range(256)] yield sum(map(_hash, steps))
for step in steps: # part 2
boxes: list[dict[str, int]] = [{} for _ in range(256)]
for step in steps:
if (i := step.find("=")) >= 0: if (i := step.find("=")) >= 0:
label, length = step[:i], int(step[i + 1 :]) label, length = step[:i], int(step[i + 1 :])
boxes[_hash(label)][label] = length boxes[_hash(label)][label] = length
@ -23,9 +26,8 @@ for step in steps:
label = step[:-1] label = step[:-1]
boxes[_hash(label)].pop(label, None) boxes[_hash(label)].pop(label, None)
answer_2 = sum( yield sum(
i_box * i_lens * length i_box * i_lens * length
for i_box, box in enumerate(boxes, start=1) for i_box, box in enumerate(boxes, start=1)
for i_lens, length in enumerate(box.values(), start=1) for i_lens, length in enumerate(box.values(), start=1)
) )
print(f"answer 2 is {answer_2}")

View File

@ -1,8 +1,6 @@
import os from typing import Any, Iterator, Literal, TypeAlias, cast
import sys
from typing import Literal, TypeAlias, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
CellType: TypeAlias = Literal[".", "|", "-", "\\", "/"] CellType: TypeAlias = Literal[".", "|", "-", "\\", "/"]
Direction: TypeAlias = Literal["R", "L", "U", "D"] Direction: TypeAlias = Literal["R", "L", "U", "D"]
@ -78,33 +76,33 @@ def propagate(
return beams return beams
layout: list[list[CellType]] = [ class Solver(BaseSolver):
[cast(CellType, col) for col in row] for row in sys.stdin.read().splitlines() 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: # part 1
print("\n".join(["".join("#" if col else "." for col in row) for row in beams])) yield sum(sum(map(bool, row)) for row in beams)
# part 1 # part 2
answer_1 = sum(sum(map(bool, row)) for row in beams) n_rows, n_cols = len(layout), len(layout[0])
print(f"answer 1 is {answer_1}") cases: list[tuple[tuple[int, int], Direction]] = []
# part 2 for row in range(n_rows):
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, 0), "R"))
cases.append(((row, n_cols - 1), "L")) cases.append(((row, n_cols - 1), "L"))
for col in range(n_cols): for col in range(n_cols):
cases.append(((0, col), "D")) cases.append(((0, col), "D"))
cases.append(((n_rows - 1, col), "U")) cases.append(((n_rows - 1, col), "U"))
answer_2 = max( yield max(
sum(sum(map(bool, row)) for row in propagate(layout, start, direction)) sum(sum(map(bool, row)) for row in propagate(layout, start, direction))
for start, direction in cases for start, direction in cases
) )
print(f"answer 2 is {answer_2}")

View File

@ -1,13 +1,11 @@
from __future__ import annotations from __future__ import annotations
import heapq import heapq
import os
import sys
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass 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"] Direction: TypeAlias = Literal[">", "<", "^", "v"]
@ -32,11 +30,13 @@ MAPPINGS: dict[Direction, tuple[int, int, Direction]] = {
} }
def print_shortest_path( class Solver(BaseSolver):
def print_shortest_path(
self,
grid: list[list[int]], grid: list[list[int]],
target: tuple[int, int], target: tuple[int, int],
per_cell: dict[tuple[int, int], list[tuple[Label, int]]], per_cell: dict[tuple[int, int], list[tuple[Label, int]]],
): ):
assert len(per_cell[target]) == 1 assert len(per_cell[target]) == 1
label = per_cell[target][0][0] label = per_cell[target][0][0]
@ -66,16 +66,18 @@ def print_shortest_path(
if (r, c) != (prev_label.row, prev_label.col): if (r, c) != (prev_label.row, prev_label.col):
p_grid[r][c] = f"\033[93m{grid[r][c]}\033[0m" 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]:
def shortest_many_paths(grid: list[list[int]]) -> dict[tuple[int, int], int]:
n_rows, n_cols = len(grid), len(grid[0]) 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]] = {}
@ -125,13 +127,13 @@ def shortest_many_paths(grid: list[list[int]]) -> dict[tuple[int, int], int]:
return {(r, c): visited[r, c][1] for r in range(n_rows) for c in range(n_cols)} return {(r, c): visited[r, c][1] for r in range(n_rows) for c in range(n_cols)}
def shortest_path(
def shortest_path( self,
grid: list[list[int]], grid: list[list[int]],
min_straight: int, min_straight: int,
max_straight: int, max_straight: int,
lower_bounds: dict[tuple[int, int], int], lower_bounds: dict[tuple[int, int], int],
) -> int: ) -> int:
n_rows, n_cols = len(grid), len(grid[0]) n_rows, n_cols = len(grid), len(grid[0])
target = (len(grid) - 1, len(grid[0]) - 1) target = (len(grid) - 1, len(grid[0]) - 1)
@ -215,19 +217,17 @@ def shortest_path(
), ),
) )
if VERBOSE: if self.verbose:
print_shortest_path(grid, target, per_cell) self.print_shortest_path(grid, target, per_cell)
return per_cell[target][0][1] return per_cell[target][0][1]
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)
data = [[int(c) for c in r] for r in sys.stdin.read().splitlines()] # part 1
estimates = shortest_many_paths(data) yield self.shortest_path(data, 1, 3, lower_bounds=estimates)
# part 1 # part 2
answer_1 = shortest_path(data, 1, 3, lower_bounds=estimates) yield self.shortest_path(data, 4, 10, 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}")

View File

@ -1,5 +1,6 @@
import sys from typing import Any, Iterator, Literal, TypeAlias, cast
from typing import Literal, TypeAlias, cast
from ..base import BaseSolver
Direction: TypeAlias = Literal["R", "L", "U", "D"] 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 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 # part 2
answer_1 = area( yield 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( *polygon(
[ [
(DIRECTIONS[int((h := line.split()[-1])[-2])], int(h[2:-2], 16)) (DIRECTIONS[int((h := line.split()[-1])[-2])], int(h[2:-2], 16))
for line in lines for line in lines
] ]
) )
) )
print(f"answer 2 is {answer_2}")

View File

@ -1,13 +1,8 @@
import logging
import operator import operator
import os
import sys
from math import prod from math import prod
from typing import Literal, TypeAlias, cast from typing import Any, Iterator, Literal, TypeAlias, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
Category: TypeAlias = Literal["x", "m", "a", "s"] Category: TypeAlias = Literal["x", "m", "a", "s"]
Part: TypeAlias = dict[Category, int] Part: TypeAlias = dict[Category, int]
@ -22,7 +17,8 @@ Check: TypeAlias = tuple[Category, Literal["<", ">"], int] | None
Workflow: TypeAlias = list[tuple[Check, str]] Workflow: TypeAlias = list[tuple[Check, str]]
def accept(workflows: dict[str, Workflow], part: Part) -> bool: class Solver(BaseSolver):
def accept(self, workflows: dict[str, Workflow], part: Part) -> bool:
workflow = "in" workflow = "in"
decision: bool | None = None decision: bool | None = None
@ -42,8 +38,7 @@ def accept(workflows: dict[str, Workflow], part: Part) -> bool:
return decision return decision
def propagate(self, workflows: dict[str, Workflow], start: PartWithBounds) -> int:
def propagate(workflows: dict[str, Workflow], start: PartWithBounds) -> int:
def _fmt(meta: PartWithBounds) -> str: def _fmt(meta: PartWithBounds) -> str:
return "{" + ", ".join(f"{k}={v}" for k, v in meta.items()) + "}" return "{" + ", ".join(f"{k}={v}" for k, v in meta.items()) + "}"
@ -52,13 +47,13 @@ def propagate(workflows: dict[str, Workflow], start: PartWithBounds) -> int:
) -> int: ) -> int:
count = 0 count = 0
if target in workflows: if target in workflows:
logging.info(f" transfer to {target}") self.logger.info(f" transfer to {target}")
queue.append((meta, target)) queue.append((meta, target))
elif target == "A": elif target == "A":
count = prod((high - low + 1) for low, high in meta.values()) count = prod((high - low + 1) for low, high in meta.values())
logging.info(f" accepted ({count})") self.logger.info(f" accepted ({count})")
else: else:
logging.info(" rejected") self.logger.info(" rejected")
return count return count
accepted = 0 accepted = 0
@ -69,24 +64,26 @@ def propagate(workflows: dict[str, Workflow], start: PartWithBounds) -> int:
while queue: while queue:
n_iterations += 1 n_iterations += 1
meta, workflow = queue.pop() meta, workflow = queue.pop()
logging.info(f"{workflow}: {_fmt(meta)}") self.logger.info(f"{workflow}: {_fmt(meta)}")
for check, target in workflows[workflow]: for check, target in workflows[workflow]:
if check is None: if check is None:
logging.info(" end-of-workflow") self.logger.info(" end-of-workflow")
accepted += transfer_or_accept(target, meta, queue) accepted += transfer_or_accept(target, meta, queue)
continue continue
category, sense, value = check category, sense, value = check
bounds, op = meta[category], OPERATORS[sense] bounds, op = meta[category], OPERATORS[sense]
logging.info(f" checking {_fmt(meta)} against {category} {sense} {value}") self.logger.info(
f" checking {_fmt(meta)} against {category} {sense} {value}"
)
if not op(bounds[0], value) and not op(bounds[1], value): if not op(bounds[0], value) and not op(bounds[1], value):
logging.info(" reject, always false") self.logger.info(" reject, always false")
continue continue
if op(meta[category][0], value) and op(meta[category][1], value): if op(meta[category][0], value) and op(meta[category][1], value):
logging.info(" accept, always true") self.logger.info(" accept, always true")
accepted += transfer_or_accept(target, meta, queue) accepted += transfer_or_accept(target, meta, queue)
break break
@ -96,18 +93,18 @@ def propagate(workflows: dict[str, Workflow], start: PartWithBounds) -> int:
meta[category], meta2[category] = (value, high), (low, value - 1) meta[category], meta2[category] = (value, high), (low, value - 1)
else: else:
meta[category], meta2[category] = (low, value), (value + 1, high) meta[category], meta2[category] = (low, value), (value + 1, high)
logging.info(f" split {_fmt(meta2)} ({target}), {_fmt(meta)}") self.logger.info(f" split {_fmt(meta2)} ({target}), {_fmt(meta)}")
accepted += transfer_or_accept(target, meta2, queue) accepted += transfer_or_accept(target, meta2, queue)
logging.info(f"run took {n_iterations} iterations") self.logger.info(f"run took {n_iterations} iterations")
return accepted return accepted
def solve(self, input: str) -> Iterator[Any]:
workflows_s, parts_s = input.split("\n\n")
workflows_s, parts_s = sys.stdin.read().strip().split("\n\n") workflows: dict[str, Workflow] = {}
for workflow_s in workflows_s.split("\n"):
workflows: dict[str, Workflow] = {}
for workflow_s in workflows_s.split("\n"):
name, block_s = workflow_s.split("{") name, block_s = workflow_s.split("{")
workflows[name] = [] workflows[name] = []
@ -124,17 +121,14 @@ for workflow_s in workflows_s.split("\n"):
check, target = None, block check, target = None, block
workflows[name].append((check, target)) workflows[name].append((check, target))
# part 1 # part 1
parts: list[Part] = [ parts: list[Part] = [
{cast(Category, s[0]): int(s[2:]) for s in part_s[1:-1].split(",")} {cast(Category, s[0]): int(s[2:]) for s in part_s[1:-1].split(",")}
for part_s in parts_s.split("\n") for part_s in parts_s.split("\n")
] ]
answer_1 = sum(sum(part.values()) for part in parts if accept(workflows, part)) yield sum(sum(part.values()) for part in parts if self.accept(workflows, part))
print(f"answer 1 is {answer_1}")
# part 2
# part 2 yield self.propagate(
answer_2 = propagate(
workflows, {cast(Category, c): (1, 4000) for c in ["x", "m", "a", "s"]} workflows, {cast(Category, c): (1, 4000) for c in ["x", "m", "a", "s"]}
) )
print(f"answer 2 is {answer_2}")

View File

@ -1,55 +1,42 @@
import logging
import os
import sys import sys
from collections import defaultdict from collections import defaultdict
from math import lcm from math import lcm
from typing import Literal, TypeAlias from typing import Any, Iterator, Literal, TypeAlias
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
from ..base import BaseSolver
ModuleType: TypeAlias = Literal["broadcaster", "conjunction", "flip-flop"] ModuleType: TypeAlias = Literal["broadcaster", "conjunction", "flip-flop"]
PulseType: TypeAlias = Literal["high", "low"] 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: def _process(
name, outputs_s = line.split(" -> ") self,
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(
start: tuple[str, str, PulseType], start: tuple[str, str, PulseType],
flip_flop_states: dict[str, Literal["on", "off"]], flip_flop_states: dict[str, Literal["on", "off"]],
conjunction_states: dict[str, dict[str, PulseType]], conjunction_states: dict[str, dict[str, PulseType]],
) -> tuple[dict[PulseType, int], dict[str, dict[PulseType, int]]]: ) -> tuple[dict[PulseType, int], dict[str, dict[PulseType, int]]]:
pulses: list[tuple[str, str, PulseType]] = [start] pulses: list[tuple[str, str, PulseType]] = [start]
counts: dict[PulseType, int] = {"low": 0, "high": 0} counts: dict[PulseType, int] = {"low": 0, "high": 0}
inputs: dict[str, dict[PulseType, int]] = defaultdict(lambda: {"low": 0, "high": 0}) inputs: dict[str, dict[PulseType, int]] = defaultdict(
lambda: {"low": 0, "high": 0}
)
logging.info("starting process... ") self.logger.info("starting process... ")
while pulses: while pulses:
input, name, pulse = pulses.pop(0) input, name, pulse = pulses.pop(0)
logging.info(f"{input} -{pulse}-> {name}") self.logger.info(f"{input} -{pulse}-> {name}")
counts[pulse] += 1 counts[pulse] += 1
inputs[name][pulse] += 1 inputs[name][pulse] += 1
if name not in modules: if name not in self._modules:
continue continue
type, outputs = modules[name] type, outputs = self._modules[name]
if type == "broadcaster": if type == "broadcaster":
... ...
@ -77,11 +64,27 @@ def process(
return counts, inputs return counts, inputs
def solve(self, input: str) -> Iterator[Any]:
self._modules = {}
with open("./day20.dot", "w") as fp: 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:
self._modules[name[1:]] = (
"conjunction" if name.startswith("&") else "flip-flop",
outputs,
)
if self.outputs:
with open("./day20.dot", "w") as fp:
fp.write("digraph G {\n") fp.write("digraph G {\n")
fp.write("rx [shape=circle, color=red, style=filled];\n") fp.write("rx [shape=circle, color=red, style=filled];\n")
for name, (type, outputs) in modules.items(): for name, (type, outputs) in self._modules.items():
if type == "conjunction": if type == "conjunction":
shape = "diamond" shape = "diamond"
elif type == "flip-flop": elif type == "flip-flop":
@ -89,58 +92,67 @@ with open("./day20.dot", "w") as fp:
else: else:
shape = "circle" shape = "circle"
fp.write(f"{name} [shape={shape}];\n") fp.write(f"{name} [shape={shape}];\n")
for name, (type, outputs) in modules.items(): for name, (type, outputs) in self._modules.items():
for output in outputs: for output in outputs:
fp.write(f"{name} -> {output};\n") fp.write(f"{name} -> {output};\n")
fp.write("}\n") fp.write("}\n")
# part 1 # part 1
flip_flop_states: dict[str, Literal["on", "off"]] = { flip_flop_states: dict[str, Literal["on", "off"]] = {
name: "off" for name, (type, _) in modules.items() if type == "flip-flop" name: "off"
} for name, (type, _) in self._modules.items()
conjunction_states: dict[str, dict[str, PulseType]] = { if type == "flip-flop"
name: {input: "low" for input, (_, outputs) in modules.items() if name in outputs} }
for name, (type, _) in modules.items() 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" if type == "conjunction"
} }
counts: dict[PulseType, int] = {"low": 0, "high": 0} counts: dict[PulseType, int] = {"low": 0, "high": 0}
for _ in range(1000): for _ in range(1000):
result, _ = process( result, _ = self._process(
("button", "broadcaster", "low"), flip_flop_states, conjunction_states ("button", "broadcaster", "low"), flip_flop_states, conjunction_states
) )
for pulse in ("low", "high"): for pulse in ("low", "high"):
counts[pulse] += result[pulse] counts[pulse] += result[pulse]
answer_1 = counts["low"] * counts["high"] yield counts["low"] * counts["high"]
print(f"answer 1 is {answer_1}")
# part 2 # part 2
# reset states # reset states
for name in flip_flop_states: for name in flip_flop_states:
flip_flop_states[name] = "off" flip_flop_states[name] = "off"
for name in conjunction_states: for name in conjunction_states:
for input in conjunction_states[name]: for input in conjunction_states[name]:
conjunction_states[name][input] = "low" conjunction_states[name][input] = "low"
# find the conjunction connected to rx # find the conjunction connected to rx
to_rx = [name for name, (_, outputs) in modules.items() if "rx" in outputs] to_rx = [
assert len(to_rx) == 1, "cannot handle multiple module inputs for rx" name for name, (_, outputs) in self._modules.items() if "rx" in outputs
assert ( ]
modules[to_rx[0]][0] == "conjunction" assert len(to_rx) == 1, "cannot handle multiple module inputs for rx"
), "can only handle conjunction as input to rx" assert (
self._modules[to_rx[0]][0] == "conjunction"
), "can only handle conjunction as input to rx"
to_rx_inputs = [name for name, (_, outputs) in modules.items() if to_rx[0] in outputs] to_rx_inputs = [
assert all( name for name, (_, outputs) in self._modules.items() if to_rx[0] in outputs
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" 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"
count = 1
count = 1 cycles: dict[str, int] = {}
cycles: dict[str, int] = {} second: dict[str, int] = {}
second: dict[str, int] = {} while len(second) != len(to_rx_inputs):
while len(second) != len(to_rx_inputs): _, inputs = self._process(
_, inputs = process(
("button", "broadcaster", "low"), flip_flop_states, conjunction_states ("button", "broadcaster", "low"), flip_flop_states, conjunction_states
) )
@ -153,9 +165,8 @@ while len(second) != len(to_rx_inputs):
count += 1 count += 1
assert all( assert all(
second[k] == cycles[k] * 2 for k in to_rx_inputs second[k] == cycles[k] * 2 for k in to_rx_inputs
), "cannot only handle cycles starting at the beginning" ), "cannot only handle cycles starting at the beginning"
answer_2 = lcm(*cycles.values()) yield lcm(*cycles.values())
print(f"answer 2 is {answer_2}")

View File

@ -1,9 +1,6 @@
import logging from typing import Any, Iterator
import os
import sys
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
def reachable( def reachable(
@ -21,35 +18,39 @@ def reachable(
return tiles return tiles
map = sys.stdin.read().splitlines() class Solver(BaseSolver):
start = next( def solve(self, input: str) -> Iterator[Any]:
(i, j) for i in range(len(map)) for j in range(len(map[i])) if map[i][j] == "S" 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"
)
# part 1 # part 1
answer_1 = len(reachable(map, {start}, 6 if len(map) < 20 else 64)) yield len(reachable(map, {start}, 6 if len(map) < 20 else 64))
print(f"answer 1 is {answer_1}")
# part 2 # part 2
# the initial map is a square and contains an empty rhombus whose diameter is the size # the initial map is a square and contains an empty rhombus whose diameter is
# of the map, and has only empty cells around the middle row and column # 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 # after ~n/2 steps, the first map is filled with a rhombus, after that we get a
# rhombus every n steps # bigger rhombus every n steps
# #
# we are going to find the number of cells reached for the initial rhombus, n steps # we are going to find the number of cells reached for the initial rhombus, n
# after and n * 2 steps after # steps after and n * 2 steps after
# #
cycle = len(map) cycle = len(map)
rhombus = (len(map) - 3) // 2 + 1 rhombus = (len(map) - 3) // 2 + 1
values: list[int] = [] values: list[int] = []
values.append(len(tiles := reachable(map, {start}, rhombus))) values.append(len(tiles := reachable(map, {start}, rhombus)))
values.append(len(tiles := reachable(map, tiles, cycle))) values.append(len(tiles := reachable(map, tiles, cycle)))
values.append(len(tiles := reachable(map, tiles, cycle))) values.append(len(tiles := reachable(map, tiles, cycle)))
if logging.root.getEffectiveLevel() == logging.INFO: if self.verbose:
n_rows, n_cols = len(map), len(map[0]) n_rows, n_cols = len(map), len(map[0])
rows = [ rows = [
@ -65,42 +66,42 @@ if logging.root.getEffectiveLevel() == logging.INFO:
if (i // cycle) % 2 == (j // cycle) % 2: if (i // cycle) % 2 == (j // cycle) % 2:
rows[i][j] = f"\033[91m{rows[i][j]}\033[0m" rows[i][j] = f"\033[91m{rows[i][j]}\033[0m"
print("\n".join("".join(row) for row in rows)) for row in rows:
self.logger.info("".join(row))
self.logger.info(f"values to fit: {values}")
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
#
# version 1: counts = [
#
# 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( sum(
(i, j) in tiles (i, j) in tiles
@ -110,40 +111,40 @@ counts = [
for cj in range(-2, 3) for cj in range(-2, 3)
] ]
for ci in range(-2, 3) for ci in range(-2, 3)
] ]
radius = (26501365 - rhombus) // cycle - 1 radius = (26501365 - rhombus) // cycle - 1
A = counts[2][2] if radius % 2 == 0 else counts[2][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] B = counts[2][2] if radius % 2 == 1 else counts[2][1]
answer_2 = ( answer_2 = (
(radius + 1) * A (radius + 1) * A
+ radius * B + radius * B
+ 2 * radius * (radius + 1) // 2 * A + 2 * radius * (radius + 1) // 2 * A
+ 2 * radius * (radius - 1) // 2 * B + 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, 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 ((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 + 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}") print(f"answer 2 (v1) is {answer_2}")
# version 2: fitting a polynomial # version 2: fitting a polynomial
# #
# the value we are interested in (26501365) can be written as R + K * C where R is the # 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 # 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 # 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 # simpler equation for the a, b and c coefficient
# #
# we get: # we get:
# - (a * 0² + b * 0 + c) = y1 => c = y1 # - (a * 0² + b * 0 + c) = y1 => c = y1
# - (a * 1² + b * 1 + c) = y2 => a + b = y2 - y1 # - (a * 1² + b * 1 + c) = y2 => a + b = y2 - y1
# => b = y2 - y1 - a # => b = y2 - y1 - a
# - (a * 2² + b * 2 + c) = y3 => 4a + 2b = y3 - y1 # - (a * 2² + b * 2 + c) = y3 => 4a + 2b = y3 - y1
# => 4a + 2(y2 - y1 - a) = y3 - y1 # => 4a + 2(y2 - y1 - a) = y3 - y1
# => a = (y1 + y3) / 2 - y2 # => a = (y1 + y3) / 2 - y2
# #
y1, y2, y3 = values y1, y2, y3 = values
a, b, c = (y1 + y3) // 2 - y2, 2 * y2 - (3 * y1 + y3) // 2, y1 a, b, c = (y1 + y3) // 2 - y2, 2 * y2 - (3 * y1 + y3) // 2, y1
n = (26501365 - rhombus) // cycle n = (26501365 - rhombus) // cycle
answer_2 = a * n * n + b * n + c yield a * n * n + b * n + c
print(f"answer 2 (v2) is {answer_2}")

View File

@ -1,26 +1,23 @@
import itertools import itertools
import logging
import os
import string import string
import sys
from collections import defaultdict from collections import defaultdict
from typing import Any, Iterator
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
def _name(i: int) -> str:
def _name(i: int) -> str:
if len(lines) < 26: if len(lines) < 26:
return string.ascii_uppercase[i] return string.ascii_uppercase[i]
return f"B{i:04d}" return f"B{i:04d}"
def build_supports(
def build_supports(
bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]], bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]],
) -> tuple[dict[int, set[int]], dict[int, set[int]]]: ) -> tuple[dict[int, set[int]], dict[int, set[int]]]:
# 1. compute locations where a brick of sand will land after falling by processing # 1. compute locations where a brick of sand will land after falling by processing
# them in sorted order of bottom z location # them in sorted order of bottom z location
levels: dict[tuple[int, int, int], int] = defaultdict(lambda: -1) levels: dict[tuple[int, int, int], int] = defaultdict(lambda: -1)
@ -42,7 +39,9 @@ def build_supports(
# 2. compute the bricks that supports any brick # 2. compute the bricks that supports any brick
supported_by: dict[int, set[int]] = {} supported_by: dict[int, set[int]] = {}
supports: dict[int, set[int]] = {i_brick: set() for i_brick in range(len(bricks))} 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): for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks):
name = _name(i_brick) name = _name(i_brick)
@ -51,7 +50,7 @@ def build_supports(
for x, y in itertools.product(range(sx, ex + 1), range(sy, ey + 1)) for x, y in itertools.product(range(sx, ex + 1), range(sy, ey + 1))
if (v := levels[x, y, sz - 1]) != -1 if (v := levels[x, y, sz - 1]) != -1
} }
logging.info( self.logger.info(
f"{name} supported by {', '.join(map(_name, supported_by[i_brick]))}" f"{name} supported by {', '.join(map(_name, supported_by[i_brick]))}"
) )
@ -60,9 +59,8 @@ def build_supports(
return supported_by, supports return supported_by, supports
bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]] = []
bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]] = [] for line in lines:
for line in lines:
bricks.append( bricks.append(
( (
tuple(int(c) for c in line.split("~")[0].split(",")), # type: ignore tuple(int(c) for c in line.split("~")[0].split(",")), # type: ignore
@ -70,20 +68,19 @@ for line in lines:
) )
) )
# sort bricks by bottom z position to compute supports # sort bricks by bottom z position to compute supports
bricks = sorted(bricks, key=lambda b: b[0][-1]) bricks = sorted(bricks, key=lambda b: b[0][-1])
supported_by, supports = build_supports(bricks) supported_by, supports = build_supports(bricks)
# part 1 # part 1
answer_1 = len(bricks) - sum( yield len(bricks) - sum(
any(len(supported_by[supported]) == 1 for supported in supports_to) any(len(supported_by[supported]) == 1 for supported in supports_to)
for supports_to in supports.values() for supports_to in supports.values()
) )
print(f"answer 1 is {answer_1}")
# part 2 # part 2
falling_in_chain: dict[int, set[int]] = {} falling_in_chain: dict[int, set[int]] = {}
for i_brick in range(len(bricks)): for i_brick in range(len(bricks)):
to_disintegrate: set[int] = { to_disintegrate: set[int] = {
supported supported
for supported in supports[i_brick] for supported in supports[i_brick]
@ -100,12 +97,13 @@ for i_brick in range(len(bricks)):
for d_brick in to_disintegrate: for d_brick in to_disintegrate:
for supported in supports[d_brick]: for supported in supports[d_brick]:
supported_by_copy[supported] = supported_by_copy[supported] - {d_brick} supported_by_copy[supported] = supported_by_copy[supported] - {
d_brick
}
if not supported_by_copy[supported]: if not supported_by_copy[supported]:
to_disintegrate_v.add(supported) to_disintegrate_v.add(supported)
to_disintegrate = to_disintegrate_v to_disintegrate = to_disintegrate_v
answer_2 = sum(len(falling) for falling in falling_in_chain.values()) yield sum(len(falling) for falling in falling_in_chain.values())
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import logging
import os
import sys
from collections import defaultdict 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" from ..base import BaseSolver
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
DirectionType: TypeAlias = Literal[">", "<", "^", "v", ".", "#"] DirectionType: TypeAlias = Literal[">", "<", "^", "v", ".", "#"]
@ -35,6 +31,7 @@ def neighbors(
Compute neighbors of the given node, ignoring the given set of nodes and considering Compute neighbors of the given node, ignoring the given set of nodes and considering
that you can go uphill on slopes. that you can go uphill on slopes.
""" """
n_rows, n_cols = len(grid), len(grid[0])
i, j = node i, j = node
for di, dj in Neighbors[grid[i][j]]: for di, dj in Neighbors[grid[i][j]]:
@ -103,11 +100,13 @@ def compute_direct_links(
return direct return direct
def longest_path_length( class Solver(BaseSolver):
def longest_path_length(
self,
links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]], links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]],
start: tuple[int, int], start: tuple[int, int],
target: tuple[int, int], target: tuple[int, int],
) -> int: ) -> int:
max_distance: int = -1 max_distance: int = -1
queue: list[tuple[tuple[int, int], int, frozenset[tuple[int, int]]]] = [ queue: list[tuple[tuple[int, int], int, frozenset[tuple[int, int]]]] = [
(start, 0, frozenset({start})) (start, 0, frozenset({start}))
@ -129,39 +128,38 @@ def longest_path_length(
if reach not in path if reach not in path
) )
logging.info(f"processed {nodes} nodes") self.logger.info(f"processed {nodes} nodes")
return max_distance return max_distance
def solve(self, input: str) -> Iterator[Any]:
lines = cast(list[Sequence[DirectionType]], input.splitlines())
lines = cast(list[Sequence[DirectionType]], sys.stdin.read().splitlines()) start = (0, 1)
n_rows, n_cols = len(lines), len(lines[0]) target = (len(lines) - 1, len(lines[0]) - 2)
start = (0, 1)
target = (len(lines) - 1, len(lines[0]) - 2)
direct_links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = {
direct_links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = {
start: [reachable(lines, start, target)] start: [reachable(lines, start, target)]
} }
direct_links.update(compute_direct_links(lines, direct_links[start][0][0], target)) direct_links.update(
compute_direct_links(lines, direct_links[start][0][0], target)
)
# part 1 # part 1
answer_1 = longest_path_length(direct_links, start, target) yield self.longest_path_length(direct_links, start, target)
print(f"answer 1 is {answer_1}")
# part 2 # part 2
reverse_links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = defaultdict( reverse_links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = (
list defaultdict(list)
) )
for origin, links in direct_links.items(): for origin, links in direct_links.items():
for destination, distance in links: for destination, distance in links:
if origin != start: if origin != start:
reverse_links[destination].append((origin, distance)) reverse_links[destination].append((origin, distance))
links = { links = {
k: direct_links.get(k, []) + reverse_links.get(k, []) k: direct_links.get(k, []) + reverse_links.get(k, [])
for k in direct_links.keys() | reverse_links.keys() for k in direct_links.keys() | reverse_links.keys()
} }
answer_2 = longest_path_length(links, start, target) yield self.longest_path_length(links, start, target)
print(f"answer 2 is {answer_2}")

View File

@ -1,22 +1,29 @@
import sys from typing import Any, Iterator
import numpy as np import numpy as np
from sympy import solve, symbols from sympy import solve, symbols
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
positions = np.array(
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
positions = np.array(
[[int(c) for c in line.split("@")[0].strip().split(", ")] for line in lines] [[int(c) for c in line.split("@")[0].strip().split(", ")] for line in lines]
) )
velocities = np.array( velocities = np.array(
[[int(c) for c in line.split("@")[1].strip().split(", ")] for line in lines] [[int(c) for c in line.split("@")[1].strip().split(", ")] for line in lines]
) )
# part 1 # part 1
low, high = [7, 27] if len(positions) <= 10 else [200000000000000, 400000000000000] low, high = (
[7, 27] if len(positions) <= 10 else [200000000000000, 400000000000000]
)
count = 0 count = 0
for i1, (p1, v1) in enumerate(zip(positions, velocities)): for i1, (p1, v1) in enumerate(zip(positions, velocities)):
p, r = p1[:2], v1[:2] p, r = p1[:2], v1[:2]
q, s = positions[i1 + 1 :, :2], velocities[i1 + 1 :, :2] q, s = positions[i1 + 1 :, :2], velocities[i1 + 1 :, :2]
@ -31,33 +38,31 @@ for i1, (p1, v1) in enumerate(zip(positions, velocities)):
c = p + np.expand_dims(t, 1) * r c = p + np.expand_dims(t, 1) * r
count += np.all((low <= c) & (c <= high), axis=1).sum() count += np.all((low <= c) & (c <= high), axis=1).sum()
yield count
answer_1 = count # part 2
print(f"answer 1 is {answer_1}") # 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
#
# part 2 # we can solve with only 3 lines since each lines contains 3
# equation # equations (x / y / z), so 3 lines give 9 equations and 9
# p1 + t1 * v1 == p0 + t1 * v0 # variables: position (3), velocities (3) and times (3).
# p2 + t2 * v2 == p0 + t2 * v0 n = 3
# p3 + t3 * v3 == p0 + t3 * v0
# ...
# pn + tn * vn == p0 + tn * v0
#
# we can solve with only 3 lines since each lines contains 3 x, y, z, vx, vy, vz, *ts = symbols(
# 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)) "x y z vx vy vz " + " ".join(f"t{i}" for i in range(n + 1))
) )
equations = [] equations = []
for i1, ti in zip(range(n), ts): 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]): 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) equations.append(p + ti * d - pi - ti * di)
r = solve(equations, [x, y, z, vx, vy, vz] + list(ts), dict=True)[0] r = solve(equations, [x, y, z, vx, vy, vz] + list(ts), dict=True)[0]
yield r[x] + r[y] + r[z]
answer_2 = r[x] + r[y] + r[z]
print(f"answer 2 is {answer_2}")

View File

@ -1,25 +1,23 @@
import sys from typing import Any, Iterator
import networkx as nx import networkx as nx
components = { from ..base import BaseSolver
(p := line.split(": "))[0]: p[1].split() for line in sys.stdin.read().splitlines()
}
targets = {t for c in components for t in components[c] if t not in components}
graph = nx.Graph() class Solver(BaseSolver):
graph.add_edges_from((u, v) for u, vs in components.items() for v in vs) 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: "nx.Graph[str]" = nx.Graph()
graph.remove_edges_from(cut) 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 c1, c2 = nx.connected_components(graph)
answer_1 = len(c1) * len(c2)
print(f"answer 1 is {answer_1}")
# part 2 # part 1
answer_2 = ... yield len(c1) * len(c2)
print(f"answer 2 is {answer_2}")

View File

@ -1,5 +1,7 @@
import math 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]: 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 return t1, t2
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
# part 1 # part 1
times = list(map(int, lines[0].split()[1:])) times = list(map(int, lines[0].split()[1:]))
distances = list(map(int, lines[1].split()[1:])) distances = list(map(int, lines[1].split()[1:]))
answer_1 = math.prod( yield math.prod(
t2 - t1 + 1 t2 - t1 + 1
for t1, t2 in ( for t1, t2 in (
extreme_times_to_beat(time, distance) extreme_times_to_beat(time, distance)
for time, distance in zip(times, distances) for time, distance in zip(times, distances)
) )
) )
print(f"answer 1 is {answer_1}")
# part 2 # part 2
time = int(lines[0].split(":")[1].strip().replace(" ", "")) time = int(lines[0].split(":")[1].strip().replace(" ", ""))
distance = int(lines[1].split(":")[1].strip().replace(" ", "")) distance = int(lines[1].split(":")[1].strip().replace(" ", ""))
t1, t2 = extreme_times_to_beat(time, distance) t1, t2 = extreme_times_to_beat(time, distance)
answer_2 = t2 - t1 + 1 yield t2 - t1 + 1
print(f"answer 2 is {answer_2}")

View File

@ -1,5 +1,7 @@
import sys
from collections import Counter, defaultdict from collections import Counter, defaultdict
from typing import Any, Iterator
from ..base import BaseSolver
class HandTypes: 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() class Solver(BaseSolver):
cards = [(t[0], int(t[1])) for line in lines if (t := line.split())] 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 # part 2
values = {card: value for value, card in enumerate("23456789TJQKA")} values = {card: value for value, card in enumerate("J23456789TQKA")}
cards.sort(key=lambda cv: extract_key(cv[0], values=values)) cards.sort(key=lambda cv: extract_key(cv[0], values=values, joker="J"))
answer_1 = sum(rank * value for rank, (_, value) in enumerate(cards, start=1)) yield 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}")

View File

@ -1,29 +1,30 @@
import itertools import itertools
import math import math
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
sequence = lines[0]
nodes = { 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(", "))} p[0]: {d: n for d, n in zip("LR", p[1].strip("()").split(", "))}
for line in lines[2:] for line in lines[2:]
if (p := line.split(" = ")) if (p := line.split(" = "))
} }
def path(start: str):
def path(start: str):
path = [start] path = [start]
it_seq = iter(itertools.cycle(sequence)) it_seq = iter(itertools.cycle(sequence))
while not path[-1].endswith("Z"): while not path[-1].endswith("Z"):
path.append(nodes[path[-1]][next(it_seq)]) path.append(nodes[path[-1]][next(it_seq)])
return path return path
# part 1
yield len(path(next(node for node in nodes if node.endswith("A")))) - 1
# part 1 # part 2
answer_1 = len(path(next(node for node in nodes if node.endswith("A")))) - 1 yield math.lcm(*(len(path(node)) - 1 for node in nodes if node.endswith("A")))
print(f"answer 1 is {answer_1}")
# 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}")

View File

@ -1,15 +1,22 @@
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] = [] class Solver(BaseSolver):
left_values: list[int] = [] def solve(self, input: str) -> Iterator[Any]:
for values in data: lines = input.splitlines()
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] diffs = [values]
while any(d != 0 for d in diffs[-1]): while any(d != 0 for d in diffs[-1]):
diffs.append([rhs - lhs for lhs, rhs in zip(diffs[-1][:-1], diffs[-1][1:])]) diffs.append(
[rhs - lhs for lhs, rhs in zip(diffs[-1][:-1], diffs[-1][1:])]
)
rhs: list[int] = [0] rhs: list[int] = [0]
lhs: list[int] = [0] lhs: list[int] = [0]
@ -20,10 +27,8 @@ for values in data:
right_values.append(rhs[-1]) right_values.append(rhs[-1])
left_values.append(lhs[-1]) left_values.append(lhs[-1])
# part 1 # part 1
answer_1 = sum(right_values) yield sum(right_values)
print(f"answer 1 is {answer_1}")
# part 2 # part 2
answer_2 = sum(left_values) yield sum(left_values)
print(f"answer 2 is {answer_2}")

View File

@ -54,7 +54,7 @@ def main():
f".{year}.day{day}", __package__ f".{year}.day{day}", __package__
).Solver ).Solver
solver = solver_class(logging.getLogger("AOC"), year, day) solver = solver_class(logging.getLogger("AOC"), verbose, year, day)
data: str data: str
if stdin: if stdin:

View File

@ -4,10 +4,14 @@ from typing import Any, Final, Iterator
class BaseSolver: 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.logger: Final = logger
self.verbose: Final = verbose
self.year: Final = year self.year: Final = year
self.day: Final = day self.day: Final = day
self.outputs = outputs
@abstractmethod @abstractmethod
def solve(self, input: str) -> Iterator[Any]: ... def solve(self, input: str) -> Iterator[Any]: ...