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,100 +1,100 @@
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]:
(i, j) lines: list[list[Symbol]] = [
for i in range(len(lines)) [cast(Symbol, symbol) for symbol in line] for line in input.splitlines()
for j in range(len(lines[0])) ]
if lines[i][j] == "S"
)
# find one of the two outputs # find starting point
ni, nj = si, sj si, sj = next(
for ni, nj, chars in ( (i, j)
(si - 1, sj, "|7F"), for i in range(len(lines))
(si + 1, sj, "|LJ"), for j in range(len(lines[0]))
(si, sj - 1, "-LF"), if lines[i][j] == "S"
(si, sj + 1, "-J7"), )
):
if lines[ni][nj] in chars:
break
# part 1 - find the loop (re-used in part 2) # find one of the two outputs
loop = [(si, sj), (ni, nj)] ni, nj = si, sj
while True: for ni, nj, chars in (
pi, pj = loop[-2] (si - 1, sj, "|7F"),
i, j = loop[-1] (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: sym = lines[i][j]
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
if (i, j) == (si, sj): if sym == "|" and pi > i or sym in "JL" and pi == i:
break 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): if (i, j) == (si, sj):
print("\033[91mS\033[0m", end="") break
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()
answer_2 = len(inside) loop.append((i, j))
print(f"answer 2 is {answer_2}")
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)

View File

@ -1,41 +1,42 @@
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):
distances: list[int] = [] def solve(self, input: str) -> Iterator[Any]:
for g1 in range(len(galaxies_y)): lines = input.splitlines()
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])
dx = sum( data = np.array([[c == "#" for c in line] for line in lines])
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) rows = {c for c in range(data.shape[0]) if not data[c, :].any()}
return sum(distances) 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 def compute_total_distance(expansion: int) -> int:
answer_1 = compute_total_distance(2) distances: list[int] = []
print(f"answer 1 is {answer_1}") 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 dx = sum(
answer_2 = compute_total_distance(1000000) 1 + (expansion - 1) * (x in columns)
print(f"answer 2 is {answer_2}") 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)

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):
count = 0 def compute_all_possible_arrangements(
self, lines: Iterable[str], repeat: int
) -> int:
count = 0
if VERBOSE: for i_line, line in enumerate(lines):
from tqdm import tqdm 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: def solve(self, input: str) -> Iterator[Any]:
parts = line.split(" ") lines = input.splitlines()
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,
)
return count # part 1
yield self.compute_all_possible_arrangements(lines, 1)
# part 2
lines = sys.stdin.read().splitlines() yield self.compute_all_possible_arrangements(lines, 5)
# 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}")

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
yield sum(
split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0)
for block in blocks
)
# part 1 # part 2
answer_1 = sum( yield sum(
split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0) split(block, axis=1, count=1) + 100 * split(block, axis=0, count=1)
for block in blocks 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}")

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,35 +33,38 @@ 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):
rocks = cycle(rocks)
if any(rocks == c for c in cycles): N = 1000000000
break 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])) cycles.append([[c for c in r] for r in rocks])
cycle_length = i_cycle - cycle_start
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( ci = cycle_start + (N - cycle_start) % cycle_length - 1
(len(rocks) - i) * sum(1 for c in row if c == "O")
for i, row in enumerate(cycles[ci]) yield sum(
) (len(rocks) - i) * sum(1 for c in row if c == "O")
print(f"answer 2 is {answer_2}") for i, row in enumerate(cycles[ci])
)

View File

@ -1,31 +1,33 @@
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
if (i := step.find("=")) >= 0: boxes: list[dict[str, int]] = [{} for _ in range(256)]
label, length = step[:i], int(step[i + 1 :])
boxes[_hash(label)][label] = length
else:
label = step[:-1]
boxes[_hash(label)].pop(label, None)
answer_2 = sum( for step in steps:
i_box * i_lens * length if (i := step.find("=")) >= 0:
for i_box, box in enumerate(boxes, start=1) label, length = step[:i], int(step[i + 1 :])
for i_lens, length in enumerate(box.values(), start=1) boxes[_hash(label)][label] = length
) else:
print(f"answer 2 is {answer_2}") 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)
)

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.append(((row, 0), "R"))
cases: list[tuple[tuple[int, int], Direction]] = [] 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): yield max(
cases.append(((row, 0), "R")) sum(sum(map(bool, row)) for row in propagate(layout, start, direction))
cases.append(((row, n_cols - 1), "L")) for start, direction in cases
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}")

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,202 +30,204 @@ MAPPINGS: dict[Direction, tuple[int, int, Direction]] = {
} }
def print_shortest_path( class Solver(BaseSolver):
grid: list[list[int]], def print_shortest_path(
target: tuple[int, int], self,
per_cell: dict[tuple[int, int], list[tuple[Label, int]]], grid: list[list[int]],
): target: tuple[int, int],
assert len(per_cell[target]) == 1 per_cell: dict[tuple[int, int], list[tuple[Label, int]]],
label = per_cell[target][0][0] ):
assert len(per_cell[target]) == 1
label = per_cell[target][0][0]
path: list[Label] = [] path: list[Label] = []
while True: while True:
path.insert(0, label) path.insert(0, label)
if label.parent is None: if label.parent is None:
break break
label = label.parent 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 i in range(len(grid)):
for j in range(len(grid[0])): for j in range(len(grid[0])):
if per_cell[i, j]: if per_cell[i, j]:
p_grid[i][j] = f"\033[94m{grid[i][j]}\033[0m" p_grid[i][j] = f"\033[94m{grid[i][j]}\033[0m"
prev_label = path[0] prev_label = path[0]
for label in path[1:]: for label in path[1:]:
for r in range( for r in range(
min(prev_label.row, label.row), max(prev_label.row, label.row) + 1 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,
): ):
if (r, c) != (prev_label.row, prev_label.col): for c in range(
p_grid[r][c] = f"\033[93m{grid[r][c]}\033[0m" 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]: visited: dict[tuple[int, int], tuple[Label, int]] = {}
n_rows, n_cols = len(grid), len(grid[0])
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]] = [ while queue and len(visited) != n_rows * n_cols:
(0, Label(row=n_rows - 1, col=n_cols - 1, direction="^", count=0)) distance, label = heapq.heappop(queue)
]
while queue and len(visited) != n_rows * n_cols: if (label.row, label.col) in visited:
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):
continue continue
heapq.heappush( visited[label.row, label.col] = (label, distance)
queue,
( 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 distance
+ sum( + sum(
grid[r][c] grid[r][c]
for r in range(min(row, label.row), max(row, label.row) + 1) 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) for c in range(min(col, label.col), max(col, label.col) + 1)
) )
- grid[row][col], - grid[label.row][label.col]
Label( )
row=row,
col=col, heapq.heappush(
direction=direction, queue,
count=0, (
parent=label, 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 if self.verbose:
elif label.count == max_straight: self.print_shortest_path(grid, target, per_cell)
continue
# same direction, keep going and increment count return per_cell[target][0][1]
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 = ( def solve(self, input: str) -> Iterator[Any]:
distance data = [[int(c) for c in r] for r in input.splitlines()]
+ sum( estimates = self.shortest_many_paths(data)
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]
)
heapq.heappush( # part 1
queue, yield self.shortest_path(data, 1, 3, lower_bounds=estimates)
(
distance_to + lower_bounds[row, col],
distance_to,
Label(
row=row,
col=col,
direction=direction,
count=count,
parent=label,
),
),
)
if VERBOSE: # part 2
print_shortest_path(grid, target, per_cell) yield self.shortest_path(data, 4, 10, lower_bounds=estimates)
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}")

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]) *polygon(
) [
print(f"answer 1 is {answer_1}") (DIRECTIONS[int((h := line.split()[-1])[-2])], int(h[2:-2], 16))
for line in lines
# 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}")

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,119 +17,118 @@ 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):
workflow = "in" def accept(self, workflows: dict[str, Workflow], part: Part) -> bool:
decision: bool | None = None workflow = "in"
decision: bool | None = None
while decision is None: while decision is None:
for check, target in workflows[workflow]: for check, target in workflows[workflow]:
passed = check is None passed = check is None
if check is not None: if check is not None:
category, sense, value = check category, sense, value = check
passed = OPERATORS[sense](part[category], value) passed = OPERATORS[sense](part[category], value)
if passed: if passed:
if target in workflows: if target in workflows:
workflow = target workflow = target
else: else:
decision = target == "A" decision = target == "A"
break 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 transfer_or_accept(
def _fmt(meta: PartWithBounds) -> str: target: str, meta: PartWithBounds, queue: list[tuple[PartWithBounds, str]]
return "{" + ", ".join(f"{k}={v}" for k, v in meta.items()) + "}" ) -> int:
count = 0
def transfer_or_accept( if target in workflows:
target: str, meta: PartWithBounds, queue: list[tuple[PartWithBounds, str]] self.logger.info(f" transfer to {target}")
) -> int: queue.append((meta, target))
count = 0 elif target == "A":
if target in workflows: count = prod((high - low + 1) for low, high in meta.values())
logging.info(f" transfer to {target}") self.logger.info(f" accepted ({count})")
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)
else: else:
meta[category], meta2[category] = (low, value), (value + 1, high) self.logger.info(" rejected")
logging.info(f" split {_fmt(meta2)} ({target}), {_fmt(meta)}") 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") n_iterations = 0
return accepted
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] = {} self.logger.info(
for workflow_s in workflows_s.split("\n"): f" checking {_fmt(meta)} against {category} {sense} {value}"
name, block_s = workflow_s.split("{") )
workflows[name] = []
for block in block_s[:-1].split(","): if not op(bounds[0], value) and not op(bounds[1], value):
check: Check self.logger.info(" reject, always false")
if (i := block.find(":")) >= 0: continue
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 if op(meta[category][0], value) and op(meta[category][1], value):
parts: list[Part] = [ self.logger.info(" accept, always true")
{cast(Category, s[0]): int(s[2:]) for s in part_s[1:-1].split(",")} accepted += transfer_or_accept(target, meta, queue)
for part_s in parts_s.split("\n") break
]
answer_1 = sum(sum(part.values()) for part in parts if accept(workflows, part))
print(f"answer 1 is {answer_1}")
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 accepted += transfer_or_accept(target, meta2, queue)
answer_2 = propagate(
workflows, {cast(Category, c): (1, 4000) for c in ["x", "m", "a", "s"]} self.logger.info(f"run took {n_iterations} iterations")
) return accepted
print(f"answer 2 is {answer_2}")
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"]}
)

View File

@ -1,161 +1,172 @@
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(", ") start: tuple[str, str, PulseType],
if name == "broadcaster": flip_flop_states: dict[str, Literal["on", "off"]],
modules["broadcaster"] = ("broadcaster", outputs) conjunction_states: dict[str, dict[str, PulseType]],
else: ) -> tuple[dict[PulseType, int], dict[str, dict[PulseType, int]]]:
modules[name[1:]] = ( pulses: list[tuple[str, str, PulseType]] = [start]
"conjunction" if name.startswith("&") else "flip-flop", counts: dict[PulseType, int] = {"low": 0, "high": 0}
outputs, inputs: dict[str, dict[PulseType, int]] = defaultdict(
lambda: {"low": 0, "high": 0}
) )
self.logger.info("starting process... ")
def process( while pulses:
start: tuple[str, str, PulseType], input, name, pulse = pulses.pop(0)
flip_flop_states: dict[str, Literal["on", "off"]], self.logger.info(f"{input} -{pulse}-> {name}")
conjunction_states: dict[str, dict[str, PulseType]], counts[pulse] += 1
) -> 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})
logging.info("starting process... ") inputs[name][pulse] += 1
while pulses: if name not in self._modules:
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":
continue continue
if flip_flop_states[name] == "off": type, outputs = self._modules[name]
flip_flop_states[name] = "on"
pulse = "high" 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: else:
flip_flop_states[name] = "off" conjunction_states[name][input] = pulse
pulse = "low"
else: if all(state == "high" for state in conjunction_states[name].values()):
conjunction_states[name][input] = pulse pulse = "low"
else:
pulse = "high"
if all(state == "high" for state in conjunction_states[name].values()): pulses.extend((name, output, pulse) for output in outputs)
pulse = "low"
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: 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: # reset states
fp.write("digraph G {\n") for name in flip_flop_states:
fp.write("rx [shape=circle, color=red, style=filled];\n") flip_flop_states[name] = "off"
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")
# part 1 for name in conjunction_states:
flip_flop_states: dict[str, Literal["on", "off"]] = { for input in conjunction_states[name]:
name: "off" for name, (type, _) in modules.items() if type == "flip-flop" conjunction_states[name][input] = "low"
}
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}")
# 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 to_rx_inputs = [
for name in flip_flop_states: name for name, (_, outputs) in self._modules.items() if to_rx[0] in outputs
flip_flop_states[name] = "off" ]
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: count = 1
for input in conjunction_states[name]: cycles: dict[str, int] = {}
conjunction_states[name][input] = "low" 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 for node in to_rx_inputs:
to_rx = [name for name, (_, outputs) in modules.items() if "rx" in outputs] if inputs[node]["low"] == 1:
assert len(to_rx) == 1, "cannot handle multiple module inputs for rx" if node not in cycles:
assert ( cycles[node] = count
modules[to_rx[0]][0] == "conjunction" elif node not in second:
), "can only handle conjunction as input to rx" second[node] = count
to_rx_inputs = [name for name, (_, outputs) in modules.items() if to_rx[0] in outputs] count += 1
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"
assert all(
second[k] == cycles[k] * 2 for k in to_rx_inputs
), "cannot only handle cycles starting at the beginning"
count = 1 yield lcm(*cycles.values())
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}")

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,129 +18,133 @@ 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)
# part 1 for i in range(len(map))
answer_1 = len(reachable(map, {start}, 6 if len(map) < 20 else 64)) for j in range(len(map[i]))
print(f"answer 1 is {answer_1}") if map[i][j] == "S"
# 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)
) )
for cj in range(-2, 3)
]
for ci in range(-2, 3)
]
radius = (26501365 - rhombus) // cycle - 1 # part 1
A = counts[2][2] if radius % 2 == 0 else counts[2][1] yield len(reachable(map, {start}, 6 if len(map) < 20 else 64))
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 # part 2
#
# 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 # the initial map is a square and contains an empty rhombus whose diameter is
answer_2 = a * n * n + b * n + c # the size of the map, and has only empty cells around the middle row and column
print(f"answer 2 (v2) is {answer_2}") #
# 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

View File

@ -1,111 +1,109 @@
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:
if len(lines) < 26:
return string.ascii_uppercase[i]
return f"B{i:04d}"
def _name(i: int) -> str: def build_supports(
if len(lines) < 26: bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]],
return string.ascii_uppercase[i] ) -> tuple[dict[int, set[int]], dict[int, set[int]]]:
return f"B{i:04d}" # 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( for z in range(sz - 1, 0, -1):
bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]], if any(levels[x, y, z] >= 0 for x, y in itertools.product(xs, ys)):
) -> tuple[dict[int, set[int]], dict[int, set[int]]]: break
# 1. compute locations where a brick of sand will land after falling by processing sz, ez = sz - 1, ez - 1
# 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) bricks[i_brick] = ((sx, sy, sz), (ex, ey, ez))
zs = range(sz, ez + 1)
for z in range(sz - 1, 0, -1): for x, y, z in itertools.product(xs, ys, zs):
if any(levels[x, y, z] >= 0 for x, y in itertools.product(xs, ys)): levels[x, y, z] = i_brick
break
sz, ez = sz - 1, ez - 1
bricks[i_brick] = ((sx, sy, sz), (ex, ey, ez)) # 2. compute the bricks that supports any brick
zs = range(sz, ez + 1) 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): supported_by[i_brick] = {
levels[x, y, z] = 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 for support in supported_by[i_brick]:
supported_by: dict[int, set[int]] = {} supports[support].add(i_brick)
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)
supported_by[i_brick] = { return supported_by, supports
v
for x, y in itertools.product(range(sx, ex + 1), range(sy, ey + 1)) bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]] = []
if (v := levels[x, y, sz - 1]) != -1 for line in lines:
} bricks.append(
logging.info( (
f"{name} supported by {', '.join(map(_name, supported_by[i_brick]))}" 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]: # part 2
supports[support].add(i_brick) 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]]] = [] to_disintegrate_v: set[int] = set()
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 for d_brick in to_disintegrate:
bricks = sorted(bricks, key=lambda b: b[0][-1]) for supported in supports[d_brick]:
supported_by, supports = build_supports(bricks) supported_by_copy[supported] = supported_by_copy[supported] - {
d_brick
}
# part 1 if not supported_by_copy[supported]:
answer_1 = len(bricks) - sum( to_disintegrate_v.add(supported)
any(len(supported_by[supported]) == 1 for supported in supports_to)
for supports_to in supports.values()
)
print(f"answer 1 is {answer_1}")
# part 2 to_disintegrate = to_disintegrate_v
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
}
supported_by_copy = dict(supported_by) yield sum(len(falling) for falling in falling_in_chain.values())
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}")

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,65 +100,66 @@ def compute_direct_links(
return direct return direct
def longest_path_length( class Solver(BaseSolver):
links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]], def longest_path_length(
start: tuple[int, int], self,
target: tuple[int, int], links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]],
) -> int: start: tuple[int, int],
max_distance: int = -1 target: tuple[int, int],
queue: list[tuple[tuple[int, int], int, frozenset[tuple[int, int]]]] = [ ) -> int:
(start, 0, frozenset({start})) max_distance: int = -1
] queue: list[tuple[tuple[int, int], int, frozenset[tuple[int, int]]]] = [
(start, 0, frozenset({start}))
]
nodes = 0 nodes = 0
while queue: while queue:
node, distance, path = queue.pop() node, distance, path = queue.pop()
nodes += 1 nodes += 1
if node == target: if node == target:
max_distance = max(distance, max_distance) max_distance = max(distance, max_distance)
continue continue
queue.extend( queue.extend(
(reach, distance + length, path | {reach}) (reach, distance + length, path | {reach})
for reach, length in links.get(node, []) for reach, length in links.get(node, [])
if reach not in path 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()) yield self.longest_path_length(links, start, target)
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}")

View File

@ -1,63 +1,68 @@
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(
[[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()
answer_1 = count class Solver(BaseSolver):
print(f"answer 1 is {answer_1}") def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
# part 2 positions = np.array(
# equation [[int(c) for c in line.split("@")[0].strip().split(", ")] for line in lines]
# p1 + t1 * v1 == p0 + t1 * v0 )
# p2 + t2 * v2 == p0 + t2 * v0 velocities = np.array(
# p3 + t3 * v3 == p0 + t3 * v0 [[int(c) for c in line.split("@")[1].strip().split(", ")] for line in lines]
# ... )
# pn + tn * vn == p0 + tn * v0
#
# we can solve with only 3 lines since each lines contains 3 # part 1
# equations (x / y / z), so 3 lines give 9 equations and 9 low, high = (
# variables: position (3), velocities (3) and times (3). [7, 27] if len(positions) <= 10 else [200000000000000, 400000000000000]
n = 3 )
x, y, z, vx, vy, vz, *ts = symbols( count = 0
"x y z vx vy vz " + " ".join(f"t{i}" for i in range(n + 1)) for i1, (p1, v1) in enumerate(zip(positions, velocities)):
) p, r = p1[:2], v1[:2]
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] q, s = positions[i1 + 1 :, :2], velocities[i1 + 1 :, :2]
answer_2 = r[x] + r[y] + r[z] rs = np.cross(r, s)
print(f"answer 2 is {answer_2}")
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]

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

View File

@ -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] = [] class Solver(BaseSolver):
left_values: list[int] = [] def solve(self, input: str) -> Iterator[Any]:
for values in data: lines = input.splitlines()
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:])])
rhs: list[int] = [0] data = [[int(c) for c in line.split()] for line in lines]
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])
right_values.append(rhs[-1]) right_values: list[int] = []
left_values.append(lhs[-1]) 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 rhs: list[int] = [0]
answer_1 = sum(right_values) lhs: list[int] = [0]
print(f"answer 1 is {answer_1}") 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 right_values.append(rhs[-1])
answer_2 = sum(left_values) left_values.append(lhs[-1])
print(f"answer 2 is {answer_2}")
# part 1
yield sum(right_values)
# part 2
yield sum(left_values)

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]: ...