Refactor code for API #3

Merged
mikael.capelle merged 13 commits from dev/refactor-for-ui into master 2024-12-08 13:06:42 +00:00
24 changed files with 1119 additions and 1085 deletions
Showing only changes of commit 664dcfe7ba - Show all commits

16
poetry.lock generated
View File

@ -1245,6 +1245,20 @@ files = [
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"]
[[package]]
name = "types-networkx"
version = "3.4.2.20241115"
description = "Typing stubs for networkx"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-networkx-3.4.2.20241115.tar.gz", hash = "sha256:d669b650cf6c6c9ec879a825449eb04a5c10742f3109177e1683f57ee49e0f59"},
{file = "types_networkx-3.4.2.20241115-py3-none-any.whl", hash = "sha256:f0c382924d6614e06bf0b1ca0b837b8f33faa58982bc086ea762efaf39aa98dd"},
]
[package.dependencies]
numpy = ">=1.20"
[[package]]
name = "typing-extensions"
version = "4.12.2"
@ -1281,4 +1295,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "b643261f91a781d77735e05f6d2ac1002867600c2df6393a9d1a15f5e1189109"
content-hash = "c91bc307ff4a5b3e8cd1976ebea211c9749fe09d563dd80861f70ce26826cda9"

View File

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

View File

@ -1,13 +1,14 @@
import os
import sys
from typing import Literal, cast
from typing import Any, Iterator, Literal, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
from ..base import BaseSolver
Symbol = Literal["|", "-", "L", "J", "7", "F", ".", "S"]
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines: list[list[Symbol]] = [
[cast(Symbol, symbol) for symbol in line] for line in sys.stdin.read().splitlines()
[cast(Symbol, symbol) for symbol in line] for line in input.splitlines()
]
# find starting point
@ -51,8 +52,7 @@ while True:
loop.append((i, j))
answer_1 = len(loop) // 2
print(f"answer 1 is {answer_1}")
yield len(loop) // 2
# part 2
@ -83,18 +83,18 @@ for i in range(len(lines)):
if (i, j) in loop_s and lines[i][j] in "|LJ":
cnt += 1
if VERBOSE:
if self.verbose:
for i in range(len(lines)):
s = ""
for j in range(len(lines[0])):
if (i, j) == (si, sj):
print("\033[91mS\033[0m", end="")
s += "\033[91mS\033[0m"
elif (i, j) in loop:
print(lines[i][j], end="")
s += lines[i][j]
elif (i, j) in inside:
print("\033[92mI\033[0m", end="")
s += "\033[92mI\033[0m"
else:
print(".", end="")
print()
s += "."
self.logger.info(s)
answer_2 = len(inside)
print(f"answer 2 is {answer_2}")
yield len(inside)

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import sys
from typing import Callable, Literal
from typing import Any, Callable, Iterator, Literal
from ..base import BaseSolver
def split(block: list[str], axis: Literal[0, 1], count: int) -> int:
@ -25,19 +26,18 @@ def split(block: list[str], axis: Literal[0, 1], count: int) -> int:
return 0
blocks = [block.splitlines() for block in sys.stdin.read().split("\n\n")]
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
blocks = [block.splitlines() for block in input.split("\n\n")]
# part 1
answer_1 = sum(
yield sum(
split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0)
for block in blocks
)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = sum(
yield 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 TypeAlias
from typing import Any, Iterator, TypeAlias
from ..base import BaseSolver
RockGrid: TypeAlias = list[list[str]]
rocks0 = [list(line) for line in sys.stdin.read().splitlines()]
def slide_rocks_top(rocks: RockGrid) -> RockGrid:
top = [0 if c == "." else 1 for c in rocks[0]]
@ -34,13 +33,17 @@ def cycle(rocks: RockGrid) -> RockGrid:
return rocks
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
rocks0 = [list(line) for line in input.splitlines()]
rocks = slide_rocks_top([[c for c in r] for r in rocks0])
# part 1
answer_1 = sum(
(len(rocks) - i) * sum(1 for c in row if c == "O") for i, row in enumerate(rocks)
yield 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
rocks = rocks0
@ -61,8 +64,7 @@ cycle_length = i_cycle - cycle_start
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")
for i, row in enumerate(cycles[ci])
)
print(f"answer 2 is {answer_2}")

View File

@ -1,16 +1,19 @@
import sys
from functools import reduce
from typing import Any, Iterator
steps = sys.stdin.read().strip().split(",")
from ..base import BaseSolver
def _hash(s: str) -> int:
return reduce(lambda v, u: ((v + ord(u)) * 17) % 256, s, 0)
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
steps = input.split(",")
# part 1
answer_1 = sum(map(_hash, steps))
print(f"answer 1 is {answer_1}")
yield sum(map(_hash, steps))
# part 2
boxes: list[dict[str, int]] = [{} for _ in range(256)]
@ -23,9 +26,8 @@ for step in steps:
label = step[:-1]
boxes[_hash(label)].pop(label, None)
answer_2 = sum(
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)
)
print(f"answer 2 is {answer_2}")

View File

@ -1,8 +1,6 @@
import os
import sys
from typing import Literal, TypeAlias, cast
from typing import Any, Iterator, Literal, TypeAlias, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
from ..base import BaseSolver
CellType: TypeAlias = Literal[".", "|", "-", "\\", "/"]
Direction: TypeAlias = Literal["R", "L", "U", "D"]
@ -78,19 +76,20 @@ def propagate(
return beams
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
layout: list[list[CellType]] = [
[cast(CellType, col) for col in row] for row in sys.stdin.read().splitlines()
[cast(CellType, col) for col in row] for row in input.splitlines()
]
beams = propagate(layout, (0, 0), "R")
if VERBOSE:
print("\n".join(["".join("#" if col else "." for col in row) for row in beams]))
if self.verbose:
for row in beams:
self.logger.info("".join("#" if col else "." for col in row))
# part 1
answer_1 = sum(sum(map(bool, row)) for row in beams)
print(f"answer 1 is {answer_1}")
yield sum(sum(map(bool, row)) for row in beams)
# part 2
n_rows, n_cols = len(layout), len(layout[0])
@ -103,8 +102,7 @@ for col in range(n_cols):
cases.append(((0, col), "D"))
cases.append(((n_rows - 1, col), "U"))
answer_2 = max(
yield 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
import heapq
import os
import sys
from collections import defaultdict
from dataclasses import dataclass
from typing import Literal, TypeAlias
from typing import Any, Iterator, Literal, TypeAlias
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
from ..base import BaseSolver
Direction: TypeAlias = Literal[">", "<", "^", "v"]
@ -32,7 +30,9 @@ MAPPINGS: dict[Direction, tuple[int, int, Direction]] = {
}
class Solver(BaseSolver):
def print_shortest_path(
self,
grid: list[list[int]],
target: tuple[int, int],
per_cell: dict[tuple[int, int], list[tuple[Label, int]]],
@ -66,16 +66,18 @@ def print_shortest_path(
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
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(grid: list[list[int]]) -> dict[tuple[int, int], int]:
def shortest_many_paths(self, grid: list[list[int]]) -> dict[tuple[int, int], int]:
n_rows, n_cols = len(grid), len(grid[0])
visited: dict[tuple[int, int], tuple[Label, int]] = {}
@ -125,8 +127,8 @@ 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)}
def shortest_path(
self,
grid: list[list[int]],
min_straight: int,
max_straight: int,
@ -215,19 +217,17 @@ def shortest_path(
),
)
if VERBOSE:
print_shortest_path(grid, target, per_cell)
if self.verbose:
self.print_shortest_path(grid, target, per_cell)
return per_cell[target][0][1]
data = [[int(c) for c in r] for r in sys.stdin.read().splitlines()]
estimates = shortest_many_paths(data)
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)
# part 1
answer_1 = shortest_path(data, 1, 3, lower_bounds=estimates)
print(f"answer 1 is {answer_1}")
yield self.shortest_path(data, 1, 3, lower_bounds=estimates)
# part 2
answer_2 = shortest_path(data, 4, 10, lower_bounds=estimates)
print(f"answer 2 is {answer_2}")
yield self.shortest_path(data, 4, 10, lower_bounds=estimates)

View File

@ -1,5 +1,6 @@
import sys
from typing import Literal, TypeAlias, cast
from typing import Any, Iterator, Literal, TypeAlias, cast
from ..base import BaseSolver
Direction: TypeAlias = Literal["R", "L", "U", "D"]
@ -33,17 +34,19 @@ def polygon(values: list[tuple[Direction, int]]) -> tuple[list[tuple[int, int]],
return corners, perimeter
lines = sys.stdin.read().splitlines()
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
# part 1
answer_1 = area(
*polygon([(cast(Direction, (p := line.split())[0]), int(p[1])) for line in lines])
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(
yield area(
*polygon(
[
(DIRECTIONS[int((h := line.split()[-1])[-2])], int(h[2:-2], 16))
@ -51,4 +54,3 @@ answer_2 = area(
]
)
)
print(f"answer 2 is {answer_2}")

View File

@ -1,13 +1,8 @@
import logging
import operator
import os
import sys
from math import prod
from typing import Literal, TypeAlias, cast
from typing import Any, Iterator, Literal, TypeAlias, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
from ..base import BaseSolver
Category: TypeAlias = Literal["x", "m", "a", "s"]
Part: TypeAlias = dict[Category, int]
@ -22,7 +17,8 @@ Check: TypeAlias = tuple[Category, Literal["<", ">"], int] | None
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"
decision: bool | None = None
@ -42,8 +38,7 @@ def accept(workflows: dict[str, Workflow], part: Part) -> bool:
return decision
def propagate(workflows: dict[str, Workflow], start: PartWithBounds) -> int:
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()) + "}"
@ -52,13 +47,13 @@ def propagate(workflows: dict[str, Workflow], start: PartWithBounds) -> int:
) -> int:
count = 0
if target in workflows:
logging.info(f" transfer to {target}")
self.logger.info(f" transfer to {target}")
queue.append((meta, target))
elif target == "A":
count = prod((high - low + 1) for low, high in meta.values())
logging.info(f" accepted ({count})")
self.logger.info(f" accepted ({count})")
else:
logging.info(" rejected")
self.logger.info(" rejected")
return count
accepted = 0
@ -69,24 +64,26 @@ def propagate(workflows: dict[str, Workflow], start: PartWithBounds) -> int:
while queue:
n_iterations += 1
meta, workflow = queue.pop()
logging.info(f"{workflow}: {_fmt(meta)}")
self.logger.info(f"{workflow}: {_fmt(meta)}")
for check, target in workflows[workflow]:
if check is None:
logging.info(" end-of-workflow")
self.logger.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}")
self.logger.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")
self.logger.info(" reject, always false")
continue
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)
break
@ -96,15 +93,15 @@ def propagate(workflows: dict[str, Workflow], start: PartWithBounds) -> int:
meta[category], meta2[category] = (value, high), (low, value - 1)
else:
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)
logging.info(f"run took {n_iterations} iterations")
self.logger.info(f"run took {n_iterations} iterations")
return accepted
workflows_s, parts_s = sys.stdin.read().strip().split("\n\n")
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"):
@ -129,12 +126,9 @@ parts: list[Part] = [
{cast(Category, s[0]): int(s[2:]) for s in part_s[1:-1].split(",")}
for part_s in parts_s.split("\n")
]
answer_1 = sum(sum(part.values()) for part in parts if accept(workflows, part))
print(f"answer 1 is {answer_1}")
yield sum(sum(part.values()) for part in parts if self.accept(workflows, part))
# part 2
answer_2 = propagate(
yield self.propagate(
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
from collections import defaultdict
from math import lcm
from typing import Literal, TypeAlias
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
from typing import Any, Iterator, Literal, TypeAlias
from ..base import BaseSolver
ModuleType: TypeAlias = Literal["broadcaster", "conjunction", "flip-flop"]
PulseType: TypeAlias = Literal["high", "low"]
modules: dict[str, tuple[ModuleType, list[str]]] = {}
lines = sys.stdin.read().splitlines()
class Solver(BaseSolver):
_modules: dict[str, tuple[ModuleType, list[str]]]
for line in lines:
name, outputs_s = line.split(" -> ")
outputs = outputs_s.split(", ")
if name == "broadcaster":
modules["broadcaster"] = ("broadcaster", outputs)
else:
modules[name[1:]] = (
"conjunction" if name.startswith("&") else "flip-flop",
outputs,
)
def process(
def _process(
self,
start: tuple[str, str, PulseType],
flip_flop_states: dict[str, Literal["on", "off"]],
conjunction_states: dict[str, dict[str, PulseType]],
) -> tuple[dict[PulseType, int], dict[str, dict[PulseType, int]]]:
pulses: list[tuple[str, str, PulseType]] = [start]
counts: dict[PulseType, int] = {"low": 0, "high": 0}
inputs: dict[str, dict[PulseType, int]] = defaultdict(lambda: {"low": 0, "high": 0})
inputs: dict[str, dict[PulseType, int]] = defaultdict(
lambda: {"low": 0, "high": 0}
)
logging.info("starting process... ")
self.logger.info("starting process... ")
while pulses:
input, name, pulse = pulses.pop(0)
logging.info(f"{input} -{pulse}-> {name}")
self.logger.info(f"{input} -{pulse}-> {name}")
counts[pulse] += 1
inputs[name][pulse] += 1
if name not in modules:
if name not in self._modules:
continue
type, outputs = modules[name]
type, outputs = self._modules[name]
if type == "broadcaster":
...
@ -77,11 +64,27 @@ def process(
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:
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("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":
shape = "diamond"
elif type == "flip-flop":
@ -89,29 +92,34 @@ with open("./day20.dot", "w") as fp:
else:
shape = "circle"
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:
fp.write(f"{name} -> {output};\n")
fp.write("}\n")
# part 1
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()
if type == "flip-flop"
}
conjunction_states: dict[str, dict[str, PulseType]] = {
name: {input: "low" for input, (_, outputs) in modules.items() if name in outputs}
for name, (type, _) in modules.items()
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, _ = process(
result, _ = self._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}")
yield counts["low"] * counts["high"]
# part 2
@ -124,23 +132,27 @@ for name in conjunction_states:
conjunction_states[name][input] = "low"
# find the conjunction connected to rx
to_rx = [name for name, (_, outputs) in modules.items() if "rx" in outputs]
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 (
modules[to_rx[0]][0] == "conjunction"
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 = [
name for name, (_, outputs) in self._modules.items() if to_rx[0] in outputs
]
assert all(
modules[i][0] == "conjunction" and len(modules[i][1]) == 1 for i in to_rx_inputs
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
cycles: dict[str, int] = {}
second: dict[str, int] = {}
while len(second) != len(to_rx_inputs):
_, inputs = process(
_, inputs = self._process(
("button", "broadcaster", "low"), flip_flop_states, conjunction_states
)
@ -157,5 +169,4 @@ assert all(
second[k] == cycles[k] * 2 for k in to_rx_inputs
), "cannot only handle cycles starting at the beginning"
answer_2 = lcm(*cycles.values())
print(f"answer 2 is {answer_2}")
yield lcm(*cycles.values())

View File

@ -1,9 +1,6 @@
import logging
import os
import sys
from typing import Any, Iterator
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
from ..base import BaseSolver
def reachable(
@ -21,25 +18,29 @@ def reachable(
return tiles
map = sys.stdin.read().splitlines()
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
map = input.splitlines()
start = next(
(i, j) for i in range(len(map)) for j in range(len(map[i])) if map[i][j] == "S"
(i, j)
for i in range(len(map))
for j in range(len(map[i]))
if map[i][j] == "S"
)
# part 1
answer_1 = len(reachable(map, {start}, 6 if len(map) < 20 else 64))
print(f"answer 1 is {answer_1}")
yield len(reachable(map, {start}, 6 if len(map) < 20 else 64))
# 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
# 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
# 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
# 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
@ -49,7 +50,7 @@ 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:
if self.verbose:
n_rows, n_cols = len(map), len(map[0])
rows = [
@ -65,10 +66,10 @@ if logging.root.getEffectiveLevel() == logging.INFO:
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))
for row in rows:
self.logger.info("".join(row))
logging.info(f"values to fit: {values}")
self.logger.info(f"values to fit: {values}")
# version 1:
#
@ -121,7 +122,8 @@ answer_2 = (
+ 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 ((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}")
@ -145,5 +147,4 @@ y1, y2, y3 = values
a, b, c = (y1 + y3) // 2 - y2, 2 * y2 - (3 * y1 + y3) // 2, y1
n = (26501365 - rhombus) // cycle
answer_2 = a * n * n + b * n + c
print(f"answer 2 (v2) is {answer_2}")
yield a * n * n + b * n + c

View File

@ -1,23 +1,20 @@
import itertools
import logging
import os
import string
import sys
from collections import defaultdict
from typing import Any, Iterator
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
from ..base import BaseSolver
lines = sys.stdin.read().splitlines()
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
def _name(i: int) -> str:
if len(lines) < 26:
return string.ascii_uppercase[i]
return f"B{i:04d}"
def build_supports(
bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]],
) -> tuple[dict[int, set[int]], dict[int, set[int]]]:
@ -42,7 +39,9 @@ def build_supports(
# 2. compute the bricks that supports any brick
supported_by: dict[int, set[int]] = {}
supports: dict[int, set[int]] = {i_brick: set() for i_brick in range(len(bricks))}
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)
@ -51,7 +50,7 @@ def build_supports(
for x, y in itertools.product(range(sx, ex + 1), range(sy, ey + 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]))}"
)
@ -60,7 +59,6 @@ def build_supports(
return supported_by, supports
bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]] = []
for line in lines:
bricks.append(
@ -75,11 +73,10 @@ bricks = sorted(bricks, key=lambda b: b[0][-1])
supported_by, supports = build_supports(bricks)
# part 1
answer_1 = len(bricks) - sum(
yield len(bricks) - sum(
any(len(supported_by[supported]) == 1 for supported in supports_to)
for supports_to in supports.values()
)
print(f"answer 1 is {answer_1}")
# part 2
falling_in_chain: dict[int, set[int]] = {}
@ -100,12 +97,13 @@ for i_brick in range(len(bricks)):
for d_brick in to_disintegrate:
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]:
to_disintegrate_v.add(supported)
to_disintegrate = to_disintegrate_v
answer_2 = sum(len(falling) for falling in falling_in_chain.values())
print(f"answer 2 is {answer_2}")
yield sum(len(falling) for falling in falling_in_chain.values())

View File

@ -1,11 +1,7 @@
import logging
import os
import sys
from collections import defaultdict
from typing import Literal, Sequence, TypeAlias, cast
from typing import Any, Iterator, Literal, Sequence, TypeAlias, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
from ..base import BaseSolver
DirectionType: TypeAlias = Literal[">", "<", "^", "v", ".", "#"]
@ -35,6 +31,7 @@ def neighbors(
Compute neighbors of the given node, ignoring the given set of nodes and considering
that you can go uphill on slopes.
"""
n_rows, n_cols = len(grid), len(grid[0])
i, j = node
for di, dj in Neighbors[grid[i][j]]:
@ -103,7 +100,9 @@ def compute_direct_links(
return direct
class Solver(BaseSolver):
def longest_path_length(
self,
links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]],
start: tuple[int, int],
target: tuple[int, int],
@ -129,29 +128,29 @@ def longest_path_length(
if reach not in path
)
logging.info(f"processed {nodes} nodes")
self.logger.info(f"processed {nodes} nodes")
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())
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))
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}")
yield self.longest_path_length(direct_links, start, target)
# part 2
reverse_links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = defaultdict(
list
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:
@ -163,5 +162,4 @@ links = {
for k in direct_links.keys() | reverse_links.keys()
}
answer_2 = longest_path_length(links, start, target)
print(f"answer 2 is {answer_2}")
yield self.longest_path_length(links, start, target)

View File

@ -1,9 +1,14 @@
import sys
from typing import Any, Iterator
import numpy as np
from sympy import solve, symbols
lines = sys.stdin.read().splitlines()
from ..base import BaseSolver
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]
@ -13,7 +18,9 @@ velocities = np.array(
)
# 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
for i1, (p1, v1) in enumerate(zip(positions, velocities)):
@ -31,9 +38,7 @@ for i1, (p1, v1) in enumerate(zip(positions, velocities)):
c = p + np.expand_dims(t, 1) * r
count += np.all((low <= c) & (c <= high), axis=1).sum()
answer_1 = count
print(f"answer 1 is {answer_1}")
yield count
# part 2
# equation
@ -54,10 +59,10 @@ x, y, z, vx, vy, vz, *ts = symbols(
)
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]):
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]
answer_2 = r[x] + r[y] + r[z]
print(f"answer 2 is {answer_2}")
yield r[x] + r[y] + r[z]

View File

@ -1,14 +1,17 @@
import sys
from typing import Any, Iterator
import networkx as nx
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
components = {
(p := line.split(": "))[0]: p[1].split() for line in sys.stdin.read().splitlines()
(p := line.split(": "))[0]: p[1].split() for line in input.splitlines()
}
targets = {t for c in components for t in components[c] if t not in components}
graph = nx.Graph()
graph: "nx.Graph[str]" = nx.Graph()
graph.add_edges_from((u, v) for u, vs in components.items() for v in vs)
cut = nx.minimum_edge_cut(graph)
@ -17,9 +20,4 @@ graph.remove_edges_from(cut)
c1, c2 = nx.connected_components(graph)
# part 1
answer_1 = len(c1) * len(c2)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")
yield len(c1) * len(c2)

View File

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

View File

@ -1,5 +1,7 @@
import sys
from collections import Counter, defaultdict
from typing import Any, Iterator
from ..base import BaseSolver
class HandTypes:
@ -32,18 +34,17 @@ def extract_key(hand: str, values: dict[str, int], joker: str = "0") -> tuple[in
)
lines = sys.stdin.read().splitlines()
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
cards = [(t[0], int(t[1])) for line in lines if (t := line.split())]
# part 1
values = {card: value for value, card in enumerate("23456789TJQKA")}
cards.sort(key=lambda cv: extract_key(cv[0], values=values))
answer_1 = sum(rank * value for rank, (_, value) in enumerate(cards, start=1))
print(f"answer 1 is {answer_1}")
yield sum(rank * value for rank, (_, value) in enumerate(cards, start=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}")
yield sum(rank * value for rank, (_, value) in enumerate(cards, start=1))

View File

@ -1,8 +1,13 @@
import itertools
import math
import sys
from typing import Any, Iterator
lines = sys.stdin.read().splitlines()
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
sequence = lines[0]
nodes = {
@ -11,7 +16,6 @@ nodes = {
if (p := line.split(" = "))
}
def path(start: str):
path = [start]
it_seq = iter(itertools.cycle(sequence))
@ -19,11 +23,8 @@ def path(start: str):
path.append(nodes[path[-1]][next(it_seq)])
return path
# part 1
answer_1 = len(path(next(node for node in nodes if node.endswith("A")))) - 1
print(f"answer 1 is {answer_1}")
yield len(path(next(node for node in nodes if node.endswith("A")))) - 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}")
yield math.lcm(*(len(path(node)) - 1 for node in nodes if node.endswith("A")))

View File

@ -1,6 +1,11 @@
import sys
from typing import Any, Iterator
lines = sys.stdin.read().splitlines()
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
data = [[int(c) for c in line.split()] for line in lines]
@ -9,7 +14,9 @@ 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:])])
diffs.append(
[rhs - lhs for lhs, rhs in zip(diffs[-1][:-1], diffs[-1][1:])]
)
rhs: list[int] = [0]
lhs: list[int] = [0]
@ -21,9 +28,7 @@ for values in data:
left_values.append(lhs[-1])
# part 1
answer_1 = sum(right_values)
print(f"answer 1 is {answer_1}")
yield sum(right_values)
# part 2
answer_2 = sum(left_values)
print(f"answer 2 is {answer_2}")
yield sum(left_values)

View File

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

View File

@ -4,10 +4,14 @@ from typing import Any, Final, Iterator
class BaseSolver:
def __init__(self, logger: Logger, year: int, day: int):
def __init__(
self, logger: Logger, verbose: bool, year: int, day: int, outputs: bool = False
):
self.logger: Final = logger
self.verbose: Final = verbose
self.year: Final = year
self.day: Final = day
self.outputs = outputs
@abstractmethod
def solve(self, input: str) -> Iterator[Any]: ...