Refactor code for API (#3)

Co-authored-by: Mikael CAPELLE <mikael.capelle@thalesaleniaspace.com>
Co-authored-by: Mikaël Capelle <capelle.mikael@gmail.com>
Reviewed-on: #3
This commit is contained in:
Mikaël Capelle 2024-12-08 13:06:41 +00:00
parent ab4e3e199c
commit ce315b8778
130 changed files with 4599 additions and 3336 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,10 +1,12 @@
import sys from typing import Any, Iterator
line = sys.stdin.read().strip() from ..base import BaseSolver
floor = 0
floors = [(floor := floor + (1 if c == "(" else -1)) for c in line]
print(f"answer 1 is {floors[-1]}") class Solver(BaseSolver):
print(f"answer 2 is {floors.index(-1)}") def solve(self, input: str) -> Iterator[Any]:
floor = 0
floors = [(floor := floor + (1 if c == "(" else -1)) for c in input]
yield floors[-1]
yield floors.index(-1)

View File

@ -1,7 +1,7 @@
import itertools import itertools
import sys from typing import Any, Iterator
line = sys.stdin.read().strip() from ..base import BaseSolver
# see http://www.se16.info/js/lands2.htm for the explanation of 'atoms' (or elements) # see http://www.se16.info/js/lands2.htm for the explanation of 'atoms' (or elements)
# #
@ -9,7 +9,7 @@ line = sys.stdin.read().strip()
# CodeGolf answer https://codegolf.stackexchange.com/a/8479/42148 # CodeGolf answer https://codegolf.stackexchange.com/a/8479/42148
# fmt: off # fmt: off
atoms = [ ATOMS: list[tuple[str, tuple[int, ...]]] = [
("22", (0, )), # 0 ("22", (0, )), # 0
("13112221133211322112211213322112", (71, 90, 0, 19, 2, )), # 1 ("13112221133211322112211213322112", (71, 90, 0, 19, 2, )), # 1
("312211322212221121123222112", (1, )), # 2 ("312211322212221121123222112", (1, )), # 2
@ -105,7 +105,7 @@ atoms = [
] ]
# fmt: on # fmt: on
starters = [ STARTERS = [
"1", "1",
"11", "11",
"21", "21",
@ -122,27 +122,26 @@ def look_and_say_length(s: str, n: int) -> int:
if n == 0: if n == 0:
return len(s) return len(s)
if s in starters: if s in STARTERS:
return look_and_say_length( return look_and_say_length(
"".join(f"{len(list(g))}{k}" for k, g in itertools.groupby(s)), n - 1 "".join(f"{len(list(g))}{k}" for k, g in itertools.groupby(s)), n - 1
) )
counts = {i: 0 for i in range(len(atoms))} counts = {i: 0 for i in range(len(ATOMS))}
idx = next(i for i, (a, _) in enumerate(atoms) if s == a) idx = next(i for i, (a, _) in enumerate(ATOMS) if s == a)
counts[idx] = 1 counts[idx] = 1
for _ in range(n): for _ in range(n):
c2 = {i: 0 for i in range(len(atoms))} c2 = {i: 0 for i in range(len(ATOMS))}
for i in counts: for i in counts:
for j in atoms[i][1]: for j in ATOMS[i][1]:
c2[j] += counts[i] c2[j] += counts[i]
counts = c2 counts = c2
return sum(counts[i] * len(a[0]) for i, a in enumerate(atoms)) return sum(counts[i] * len(a[0]) for i, a in enumerate(ATOMS))
answer_1 = look_and_say_length(line, 40) class Solver(BaseSolver):
print(f"answer 1 is {answer_1}") def solve(self, input: str) -> Iterator[Any] | None:
yield look_and_say_length(input, 40)
answer_2 = look_and_say_length(line, 50) yield look_and_say_length(input, 50)
print(f"answer 2 is {answer_2}")

View File

@ -1,5 +1,7 @@
import itertools import itertools
import sys from typing import Any, Iterator
from ..base import BaseSolver
def is_valid(p: str) -> bool: def is_valid(p: str) -> bool:
@ -40,10 +42,8 @@ def find_next_password(p: str) -> str:
return p return p
line = sys.stdin.read().strip() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
answer_1 = find_next_password(line) answer_1 = find_next_password(input)
print(f"answer 1 is {answer_1}") yield answer_1
yield find_next_password(increment(answer_1))
answer_2 = find_next_password(increment(answer_1))
print(f"answer 2 is {answer_2}")

View File

@ -1,6 +1,7 @@
import json import json
import sys from typing import Any, Iterator, TypeAlias
from typing import TypeAlias
from ..base import BaseSolver
JsonObject: TypeAlias = dict[str, "JsonObject"] | list["JsonObject"] | int | str JsonObject: TypeAlias = dict[str, "JsonObject"] | list["JsonObject"] | int | str
@ -18,10 +19,9 @@ def json_sum(value: JsonObject, ignore: str | None = None) -> int:
return 0 return 0
data: JsonObject = json.load(sys.stdin) class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
data: JsonObject = json.loads(input)
answer_1 = json_sum(data) yield json_sum(data)
print(f"answer 1 is {answer_1}") yield json_sum(data, "red")
answer_2 = json_sum(data, "red")
print(f"answer 2 is {answer_2}")

View File

@ -1,10 +1,11 @@
import itertools import itertools
import sys
from collections import defaultdict from collections import defaultdict
from typing import Literal, cast from typing import Any, Iterator, Literal, cast
import parse # type: ignore import parse # type: ignore
from ..base import BaseSolver
def max_change_in_happiness(happiness: dict[str, dict[str, int]]) -> int: def max_change_in_happiness(happiness: dict[str, dict[str, int]]) -> int:
guests = list(happiness) guests = list(happiness)
@ -17,10 +18,12 @@ def max_change_in_happiness(happiness: dict[str, dict[str, int]]) -> int:
) )
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
happiness: dict[str, dict[str, int]] = defaultdict(dict) happiness: dict[str, dict[str, int]] = defaultdict(dict)
for line in lines: for line in lines:
u1, gain_or_loose, hap, u2 = cast( u1, gain_or_loose, hap, u2 = cast(
tuple[str, Literal["gain", "lose"], int, str], tuple[str, Literal["gain", "lose"], int, str],
parse.parse( # type: ignore parse.parse( # type: ignore
@ -29,13 +32,9 @@ for line in lines:
) )
happiness[u1][u2] = hap if gain_or_loose == "gain" else -hap happiness[u1][u2] = hap if gain_or_loose == "gain" else -hap
yield max_change_in_happiness(happiness)
answer_1 = max_change_in_happiness(happiness) for guest in list(happiness):
print(f"answer 1 is {answer_1}")
for guest in list(happiness):
happiness["me"][guest] = 0 happiness["me"][guest] = 0
happiness[guest]["me"] = 0 happiness[guest]["me"] = 0
answer_2 = max_change_in_happiness(happiness) yield max_change_in_happiness(happiness)
print(f"answer 2 is {answer_2}")

View File

@ -1,9 +1,10 @@
import sys
from dataclasses import dataclass from dataclasses import dataclass
from typing import Literal, cast from typing import Any, Iterator, Literal, cast
import parse # type: ignore import parse # type: ignore
from ..base import BaseSolver
@dataclass(frozen=True) @dataclass(frozen=True)
class Reindeer: class Reindeer:
@ -13,10 +14,12 @@ class Reindeer:
rest_time: int rest_time: int
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
reindeers: list[Reindeer] = [] reindeers: list[Reindeer] = []
for line in lines: for line in lines:
reindeer, speed, speed_time, rest_time = cast( reindeer, speed, speed_time, rest_time = cast(
tuple[str, int, int, int], tuple[str, int, int, int],
parse.parse( # type: ignore parse.parse( # type: ignore
@ -26,18 +29,20 @@ for line in lines:
), ),
) )
reindeers.append( reindeers.append(
Reindeer(name=reindeer, speed=speed, fly_time=speed_time, rest_time=rest_time) Reindeer(
name=reindeer, speed=speed, fly_time=speed_time, rest_time=rest_time
)
) )
target = 1000 if len(reindeers) <= 2 else 2503 target = 1000 if len(reindeers) <= 2 else 2503
states: dict[Reindeer, tuple[Literal["resting", "flying"], int]] = { states: dict[Reindeer, tuple[Literal["resting", "flying"], int]] = {
reindeer: ("resting", 0) for reindeer in reindeers reindeer: ("resting", 0) for reindeer in reindeers
} }
distances: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers} distances: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers}
points: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers} points: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers}
for time in range(target): for time in self.progress.wrap(range(target)):
for reindeer in reindeers: for reindeer in reindeers:
if states[reindeer][0] == "flying": if states[reindeer][0] == "flying":
distances[reindeer] += reindeer.speed distances[reindeer] += reindeer.speed
@ -54,9 +59,5 @@ for time in range(target):
else: else:
states[reindeer] = ("resting", time + reindeer.rest_time) states[reindeer] = ("resting", time + reindeer.rest_time)
yield max(distances.values())
answer_1 = max(distances.values()) yield max(points.values()) - 1
print(f"answer 1 is {answer_1}")
answer_2 = max(points.values()) - 1
print(f"answer 2 is {answer_2}")

View File

@ -1,9 +1,10 @@
import math import math
import sys from typing import Any, Iterator, Sequence, cast
from typing import Sequence, cast
import parse # type: ignore import parse # type: ignore
from ..base import BaseSolver
def score(ingredients: list[list[int]], teaspoons: Sequence[int]) -> int: def score(ingredients: list[list[int]], teaspoons: Sequence[int]) -> int:
return math.prod( return math.prod(
@ -18,10 +19,12 @@ def score(ingredients: list[list[int]], teaspoons: Sequence[int]) -> int:
) )
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
ingredients: list[list[int]] = [] ingredients: list[list[int]] = []
for line in lines: for line in lines:
_, *scores = cast( _, *scores = cast(
tuple[str, int, int, int, int, int], tuple[str, int, int, int, int, int],
parse.parse( # type: ignore parse.parse( # type: ignore
@ -32,11 +35,11 @@ for line in lines:
) )
ingredients.append(scores) ingredients.append(scores)
total_teaspoons = 100 total_teaspoons = 100
calories: list[int] = [] calories: list[int] = []
scores: list[int] = [] scores: list[int] = []
for a in range(total_teaspoons + 1): for a in range(total_teaspoons + 1):
for b in range(total_teaspoons + 1 - a): for b in range(total_teaspoons + 1 - a):
for c in range(total_teaspoons + 1 - a - b): for c in range(total_teaspoons + 1 - a - b):
teaspoons = (a, b, c, total_teaspoons - a - b - c) teaspoons = (a, b, c, total_teaspoons - a - b - c)
@ -49,9 +52,5 @@ for a in range(total_teaspoons + 1):
) )
) )
yield max(scores)
answer_1 = max(scores) yield max(score for score, calory in zip(scores, calories) if calory == 500)
print(f"answer 1 is {answer_1}")
answer_2 = max(score for score, calory in zip(scores, calories) if calory == 500)
print(f"answer 2 is {answer_2}")

View File

@ -1,8 +1,9 @@
import operator as op import operator as op
import re import re
import sys
from collections import defaultdict from collections import defaultdict
from typing import Callable from typing import Any, Callable, Iterator
from ..base import BaseSolver
MFCSAM: dict[str, int] = { MFCSAM: dict[str, int] = {
"children": 3, "children": 3,
@ -17,18 +18,10 @@ MFCSAM: dict[str, int] = {
"perfumes": 1, "perfumes": 1,
} }
lines = sys.stdin.readlines()
aunts: list[dict[str, int]] = [ def match(
{ aunts: list[dict[str, int]], operators: dict[str, Callable[[int, int], bool]]
match[1]: int(match[2]) ) -> int:
for match in re.findall(R"((?P<compound>[^:, ]+): (?P<quantity>\d+))", line)
}
for line in lines
]
def match(operators: dict[str, Callable[[int, int], bool]]) -> int:
return next( return next(
i i
for i, aunt in enumerate(aunts, start=1) for i, aunt in enumerate(aunts, start=1)
@ -36,16 +29,29 @@ def match(operators: dict[str, Callable[[int, int], bool]]) -> int:
) )
answer_1 = match(defaultdict(lambda: op.eq)) class Solver(BaseSolver):
print(f"answer 1 is {answer_1}") def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
answer_2 = match( aunts: list[dict[str, int]] = [
{
match[1]: int(match[2])
for match in re.findall(
R"((?P<compound>[^:, ]+): (?P<quantity>\d+))", line
)
}
for line in lines
]
yield match(aunts, defaultdict(lambda: op.eq))
yield match(
aunts,
defaultdict( defaultdict(
lambda: op.eq, lambda: op.eq,
trees=op.gt, trees=op.gt,
cats=op.gt, cats=op.gt,
pomeranians=op.lt, pomeranians=op.lt,
goldfish=op.lt, goldfish=op.lt,
),
) )
)
print(f"answer 2 is {answer_2}")

View File

@ -1,5 +1,6 @@
import sys from typing import Any, Iterator
from typing import Iterator
from ..base import BaseSolver
def iter_combinations(value: int, containers: list[int]) -> Iterator[tuple[int, ...]]: def iter_combinations(value: int, containers: list[int]) -> Iterator[tuple[int, ...]]:
@ -16,15 +17,18 @@ def iter_combinations(value: int, containers: list[int]) -> Iterator[tuple[int,
yield (containers[i],) + combination yield (containers[i],) + combination
containers = [int(c) for c in sys.stdin.read().split()] class Solver(BaseSolver):
total = 25 if len(containers) <= 5 else 150 def solve(self, input: str) -> Iterator[Any]:
containers = [int(c) for c in input.split()]
total = 25 if len(containers) <= 5 else 150
combinations = [combination for combination in iter_combinations(total, containers)] combinations = [
combination for combination in iter_combinations(total, containers)
]
answer_1 = len(combinations) yield len(combinations)
print(f"answer 1 is {answer_1}")
min_containers = min(len(combination) for combination in combinations) min_containers = min(len(combination) for combination in combinations)
yield sum(
answer_2 = sum(1 for combination in combinations if len(combination) == min_containers) 1 for combination in combinations if len(combination) == min_containers
print(f"answer 2 is {answer_2}") )

View File

@ -1,13 +1,18 @@
import itertools import itertools
import sys from typing import Any, Iterator
import numpy as np import numpy as np
from numpy.typing import NDArray from numpy.typing import NDArray
grid0 = np.array([[c == "#" for c in line] for line in sys.stdin.read().splitlines()]) from ..base import BaseSolver
# add an always off circle around
grid0 = np.concatenate( class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
grid0 = np.array([[c == "#" for c in line] for line in input.splitlines()])
# add an always off circle around
grid0 = np.concatenate(
[ [
np.zeros((grid0.shape[0] + 2, 1), dtype=bool), np.zeros((grid0.shape[0] + 2, 1), dtype=bool),
np.concatenate( np.concatenate(
@ -20,22 +25,21 @@ grid0 = np.concatenate(
np.zeros((grid0.shape[0] + 2, 1), dtype=bool), np.zeros((grid0.shape[0] + 2, 1), dtype=bool),
], ],
axis=1, axis=1,
) )
moves = list(itertools.product([-1, 0, 1], repeat=2)) moves = list(itertools.product([-1, 0, 1], repeat=2))
moves.remove((0, 0)) moves.remove((0, 0))
jjs, iis = np.meshgrid( jjs, iis = np.meshgrid(
np.arange(1, grid0.shape[0] - 1, dtype=int), np.arange(1, grid0.shape[0] - 1, dtype=int),
np.arange(1, grid0.shape[1] - 1, dtype=int), np.arange(1, grid0.shape[1] - 1, dtype=int),
) )
iis, jjs = iis.flatten(), jjs.flatten() iis, jjs = iis.flatten(), jjs.flatten()
ins = iis[:, None] + np.array(moves)[:, 0] ins = iis[:, None] + np.array(moves)[:, 0]
jns = jjs[:, None] + np.array(moves)[:, 1] jns = jjs[:, None] + np.array(moves)[:, 1]
def game_of_life(grid: NDArray[np.bool_]) -> NDArray[np.bool_]:
def game_of_life(grid: NDArray[np.bool_]) -> NDArray[np.bool_]:
neighbors_on = grid[ins, jns].sum(axis=1) neighbors_on = grid[ins, jns].sum(axis=1)
cells_on = grid[iis, jjs] cells_on = grid[iis, jjs]
@ -44,23 +48,19 @@ def game_of_life(grid: NDArray[np.bool_]) -> NDArray[np.bool_]:
return grid return grid
grid = grid0
grid = grid0 n_steps = 4 if len(grid) < 10 else 100
n_steps = 4 if len(grid) < 10 else 100 for _ in range(n_steps):
for _ in range(n_steps):
grid = game_of_life(grid) grid = game_of_life(grid)
answer_1 = grid.sum() yield grid.sum()
print(f"answer 1 is {answer_1}")
n_steps = 5 if len(grid) < 10 else 100
n_steps = 5 if len(grid) < 10 else 100 grid = grid0
grid = grid0 for _ in range(n_steps):
for _ in range(n_steps):
grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True
grid = game_of_life(grid) grid = game_of_life(grid)
grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True
answer_2 = sum(cell for line in grid for cell in line) yield sum(cell for line in grid for cell in line)
print(f"answer 2 is {answer_2}")

View File

@ -1,37 +1,41 @@
import sys
from collections import defaultdict from collections import defaultdict
from typing import Any, Iterator
replacements_s, molecule = sys.stdin.read().split("\n\n") from ..base import BaseSolver
REPLACEMENTS: dict[str, list[str]] = defaultdict(list)
for replacement_s in replacements_s.splitlines(): class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
replacements_s, molecule = input.split("\n\n")
REPLACEMENTS: dict[str, list[str]] = defaultdict(list)
for replacement_s in replacements_s.splitlines():
p = replacement_s.split(" => ") p = replacement_s.split(" => ")
REPLACEMENTS[p[0]].append(p[1]) REPLACEMENTS[p[0]].append(p[1])
molecule = molecule.strip() molecule = molecule.strip()
generated = [ generated = [
molecule[:i] + replacement + molecule[i + len(symbol) :] molecule[:i] + replacement + molecule[i + len(symbol) :]
for symbol, replacements in REPLACEMENTS.items() for symbol, replacements in REPLACEMENTS.items()
for replacement in replacements for replacement in replacements
for i in range(len(molecule)) for i in range(len(molecule))
if molecule[i:].startswith(symbol) if molecule[i:].startswith(symbol)
] ]
answer_1 = len(set(generated)) yield len(set(generated))
print(f"answer 1 is {answer_1}")
inversion: dict[str, str] = { inversion: dict[str, str] = {
replacement: symbol replacement: symbol
for symbol, replacements in REPLACEMENTS.items() for symbol, replacements in REPLACEMENTS.items()
for replacement in replacements for replacement in replacements
} }
# there is actually only one way to create the molecule, and we can greedily replace # there is actually only one way to create the molecule, and we can greedily replace
# tokens with their replacements, e.g., if H => OH then we can replace OH by H directly # tokens with their replacements, e.g., if H => OH then we can replace OH by H directly
# without thinking # without thinking
count = 0 count = 0
while molecule != "e": while molecule != "e":
i = 0 i = 0
m2 = "" m2 = ""
while i < len(molecule): while i < len(molecule):
@ -51,6 +55,4 @@ while molecule != "e":
# print(m2) # print(m2)
molecule = m2 molecule = m2
yield count
answer_2 = count
print(f"answer 2 is {count}")

View File

@ -1,20 +1,24 @@
import sys from typing import Any, Iterator
import numpy as np import numpy as np
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
length, width, height = np.array(
[[int(c) for c in line.split("x")] for line in lines]
).T
lw, wh, hl = (length * width, width * height, height * length) class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
length, width, height = np.array(
[[int(c) for c in line.split("x")] for line in input.splitlines()]
).T
answer_1 = np.sum(2 * (lw + wh + hl) + np.min(np.stack([lw, wh, hl]), axis=0)) lw, wh, hl = (length * width, width * height, height * length)
print(f"answer 1 is {answer_1}")
answer_2 = np.sum( yield np.sum(2 * (lw + wh + hl) + np.min(np.stack([lw, wh, hl]), axis=0))
yield np.sum(
length * width * height length * width * height
+ 2 * np.min(np.stack([length + width, length + height, height + width]), axis=0) + 2
) * np.min(
print(f"answer 2 is {answer_2}") np.stack([length + width, length + height, height + width]), axis=0
)
)

View File

@ -1,10 +1,10 @@
import itertools import itertools
import sys from typing import Any, Iterator
target = int(sys.stdin.read()) from ..base import BaseSolver
def presents(n: int, elf: int, max: int = target) -> int: def presents(n: int, elf: int, max: int) -> int:
count = 0 count = 0
k = 1 k = 1
while k * k < n: while k * k < n:
@ -21,8 +21,9 @@ def presents(n: int, elf: int, max: int = target) -> int:
return count return count
answer_1 = next(n for n in itertools.count(1) if presents(n, 10) >= target) class Solver(BaseSolver):
print(f"answer 1 is {answer_1}") def solve(self, input: str) -> Iterator[Any]:
target = int(input)
answer_2 = next(n for n in itertools.count(1) if presents(n, 11, 50) >= target) yield next(n for n in itertools.count(1) if presents(n, 10, target) >= target)
print(f"answer 2 is {answer_2}") yield next(n for n in itertools.count(1) if presents(n, 11, 50) >= target)

View File

@ -1,7 +1,8 @@
import itertools import itertools
import sys
from math import ceil from math import ceil
from typing import TypeAlias from typing import Any, Iterator, TypeAlias
from ..base import BaseSolver
Modifier: TypeAlias = tuple[str, int, int, int] Modifier: TypeAlias = tuple[str, int, int, int]
@ -33,17 +34,18 @@ RINGS: list[Modifier] = [
] ]
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
player_hp = 100 player_hp = 100
boss_attack = int(lines[1].split(":")[1].strip()) boss_attack = int(lines[1].split(":")[1].strip())
boss_armor = int(lines[2].split(":")[1].strip()) boss_armor = int(lines[2].split(":")[1].strip())
boss_hp = int(lines[0].split(":")[1].strip()) boss_hp = int(lines[0].split(":")[1].strip())
min_cost, max_cost = 1_000_000, 0
min_cost, max_cost = 1_000_000, 0 for equipments in itertools.product(WEAPONS, ARMORS, RINGS, RINGS):
for equipments in itertools.product(WEAPONS, ARMORS, RINGS, RINGS):
if equipments[-1][0] != "" and equipments[-2] == equipments[-1]: if equipments[-1][0] != "" and equipments[-2] == equipments[-1]:
continue continue
@ -58,9 +60,5 @@ for equipments in itertools.product(WEAPONS, ARMORS, RINGS, RINGS):
else: else:
max_cost = max(cost, max_cost) max_cost = max(cost, max_cost)
yield min_cost
answer_1 = min_cost yield max_cost
print(f"answer 1 is {answer_1}")
answer_2 = max_cost
print(f"answer 2 is {answer_2}")

View File

@ -1,8 +1,9 @@
from __future__ import annotations from __future__ import annotations
import heapq import heapq
import sys from typing import Any, Iterator, Literal, TypeAlias, cast
from typing import Literal, TypeAlias, cast
from ..base import BaseSolver
PlayerType: TypeAlias = Literal["player", "boss"] PlayerType: TypeAlias = Literal["player", "boss"]
SpellType: TypeAlias = Literal["magic missile", "drain", "shield", "poison", "recharge"] SpellType: TypeAlias = Literal["magic missile", "drain", "shield", "poison", "recharge"]
@ -62,17 +63,6 @@ def play(
continue continue
visited.add((player, player_hp, player_mana, player_armor, boss_hp, buffs)) visited.add((player, player_hp, player_mana, player_armor, boss_hp, buffs))
if hard_mode and player == "player":
player_hp = max(0, player_hp - 1)
if player_hp == 0:
continue
if boss_hp == 0:
winning_node = spells
continue
new_buffs: list[tuple[BuffType, int]] = [] new_buffs: list[tuple[BuffType, int]] = []
for buff, length in buffs: for buff, length in buffs:
length = length - 1 length = length - 1
@ -88,6 +78,16 @@ def play(
if length > 0: if length > 0:
new_buffs.append((buff, length)) new_buffs.append((buff, length))
if hard_mode and player == "player":
player_hp = player_hp - 1
if player_hp <= 0:
continue
if boss_hp <= 0:
winning_node = spells
continue
buffs = tuple(new_buffs) buffs = tuple(new_buffs)
if player == "boss": if player == "boss":
@ -155,23 +155,28 @@ def play(
return winning_node return winning_node
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
player_hp = 50 player_hp = 50
player_mana = 500 player_mana = 500
player_armor = 0 player_armor = 0
boss_hp = int(lines[0].split(":")[1].strip()) boss_hp = int(lines[0].split(":")[1].strip())
boss_attack = int(lines[1].split(":")[1].strip()) boss_attack = int(lines[1].split(":")[1].strip())
answer_1 = sum( yield sum(
c c
for _, c in play(player_hp, player_mana, player_armor, boss_hp, boss_attack, False) for _, c in play(
) player_hp, player_mana, player_armor, boss_hp, boss_attack, False
print(f"answer 1 is {answer_1}") )
)
# 1242 (not working) # 1242 (not working)
answer_2 = sum( yield sum(
c for _, c in play(player_hp, player_mana, player_armor, boss_hp, boss_attack, True) c
) for _, c in play(
print(f"answer 2 is {answer_2}") player_hp, player_mana, player_armor, boss_hp, boss_attack, True
)
)

View File

@ -1,7 +1,7 @@
import sys
from collections import defaultdict from collections import defaultdict
from typing import Any, Iterator
line = sys.stdin.read().strip() from ..base import BaseSolver
def process(directions: str) -> dict[tuple[int, int], int]: def process(directions: str) -> dict[tuple[int, int], int]:
@ -27,8 +27,7 @@ def process(directions: str) -> dict[tuple[int, int], int]:
return counts return counts
answer_1 = len(process(line)) class Solver(BaseSolver):
print(f"answer 1 is {answer_1}") def solve(self, input: str) -> Iterator[Any]:
yield len(process(input))
answer_2 = len(process(line[::2]) | process(line[1::2])) yield len(process(input[::2]) | process(input[1::2]))
print(f"answer 2 is {answer_2}")

View File

@ -1,16 +1,20 @@
import hashlib import hashlib
import itertools import itertools
import sys from typing import Any, Iterator
line = sys.stdin.read().strip() from ..base import BaseSolver
it = iter(itertools.count(1))
answer_1 = next(
i for i in it if hashlib.md5(f"{line}{i}".encode()).hexdigest().startswith("00000")
)
print(f"answer 1 is {answer_1}")
answer_2 = next( class Solver(BaseSolver):
i for i in it if hashlib.md5(f"{line}{i}".encode()).hexdigest().startswith("000000") def solve(self, input: str) -> Iterator[Any]:
) it = iter(itertools.count(1))
print(f"answer 2 is {answer_2}") yield next(
i
for i in it
if hashlib.md5(f"{input}{i}".encode()).hexdigest().startswith("00000")
)
yield next(
i
for i in it
if hashlib.md5(f"{input}{i}".encode()).hexdigest().startswith("000000")
)

View File

@ -1,4 +1,6 @@
import sys from typing import Any, Iterator
from ..base import BaseSolver
VOWELS = "aeiou" VOWELS = "aeiou"
FORBIDDEN = {"ab", "cd", "pq", "xy"} FORBIDDEN = {"ab", "cd", "pq", "xy"}
@ -27,10 +29,8 @@ def is_nice_2(s: str) -> bool:
return True return True
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
answer_1 = sum(map(is_nice_1, lines)) lines = input.splitlines()
print(f"answer 1 is {answer_1}") yield sum(map(is_nice_1, lines))
yield sum(map(is_nice_2, lines))
answer_2 = sum(map(is_nice_2, lines))
print(f"answer 2 is {answer_2}")

View File

@ -1,14 +1,16 @@
import sys from typing import Any, Iterator, Literal, cast
from typing import Literal, cast
import numpy as np import numpy as np
import parse # type: ignore import parse # type: ignore
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
lights_1 = np.zeros((1000, 1000), dtype=bool)
lights_2 = np.zeros((1000, 1000), dtype=int) class Solver(BaseSolver):
for line in lines: def solve(self, input: str) -> Iterator[Any]:
lights_1 = np.zeros((1000, 1000), dtype=bool)
lights_2 = np.zeros((1000, 1000), dtype=int)
for line in input.splitlines():
action, sx, sy, ex, ey = cast( action, sx, sy, ex, ey = cast(
tuple[Literal["turn on", "turn off", "toggle"], int, int, int, int], tuple[Literal["turn on", "turn off", "toggle"], int, int, int, int],
parse.parse("{} {:d},{:d} through {:d},{:d}", line), # type: ignore parse.parse("{} {:d},{:d} through {:d},{:d}", line), # type: ignore
@ -26,8 +28,5 @@ for line in lines:
lights_1[sx:ex, sy:ey] = ~lights_1[sx:ex, sy:ey] lights_1[sx:ex, sy:ey] = ~lights_1[sx:ex, sy:ey]
lights_2[sx:ex, sy:ey] += 2 lights_2[sx:ex, sy:ey] += 2
answer_1 = lights_1.sum() yield lights_1.sum()
print(f"answer 1 is {answer_1}") yield lights_2.sum()
answer_2 = lights_2.sum()
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import logging
import operator import operator
import os from typing import Any, Callable, Iterator
import sys
from typing import Callable
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
OPERATORS = { OPERATORS = {
"AND": operator.and_, "AND": operator.and_,
@ -36,12 +32,27 @@ def value_of(key: str) -> tuple[str, Callable[[dict[str, int]], int]]:
return key, lambda values: values[key] return key, lambda values: values[key]
lines = sys.stdin.read().splitlines() def process(
signals: Signals,
values: dict[str, int],
) -> dict[str, int]:
while signals:
signal = next(s for s in signals if all(p in values for p in signals[s][0]))
_, deps, command = signals[signal]
values[signal] = command(deps[0](values), deps[1](values)) % 65536
del signals[signal]
signals: Signals = {} return values
values: dict[str, int] = {"": 0}
for line in lines:
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any] | None:
lines = input.splitlines()
signals: Signals = {}
values: dict[str, int] = {"": 0}
for line in lines:
command, signal = line.split(" -> ") command, signal = line.split(" -> ")
if command.startswith("NOT"): if command.startswith("NOT"):
@ -77,25 +88,9 @@ for line in lines:
signals[signal] = ((lhs_s, rhs_s), (lhs_fn, rhs_fn), op) signals[signal] = ((lhs_s, rhs_s), (lhs_fn, rhs_fn), op)
values_1 = process(signals.copy(), values.copy())
for k in sorted(values_1):
self.logger.info(f"{k}: {values_1[k]}")
yield values_1["a"]
def process( yield process(signals.copy(), values | {"b": values_1["a"]})["a"]
signals: Signals,
values: dict[str, int],
) -> dict[str, int]:
while signals:
signal = next(s for s in signals if all(p in values for p in signals[s][0]))
_, deps, command = signals[signal]
values[signal] = command(deps[0](values), deps[1](values)) % 65536
del signals[signal]
return values
values_1 = process(signals.copy(), values.copy())
logging.info("\n" + "\n".join(f"{k}: {values_1[k]}" for k in sorted(values_1)))
answer_1 = values_1["a"]
print(f"answer 1 is {answer_1}")
values_2 = process(signals.copy(), values | {"b": values_1["a"]})
answer_2 = values_2["a"]
print(f"answer 2 is {answer_2}")

View File

@ -1,14 +1,13 @@
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)
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
answer_1 = sum( yield sum(
# left and right quotes (not in memory) # left and right quotes (not in memory)
2 2
# each \\ adds one character in the literals (compared to memory) # each \\ adds one character in the literals (compared to memory)
@ -20,10 +19,9 @@ answer_1 = sum(
# avoid \\\\x, etc., but this does not occur in the examples and the actual input # avoid \\\\x, etc., but this does not occur in the examples and the actual input
+ 3 * (line.count(R"\x") - line.count(R"\\x") + line.count(R"\\\x")) + 3 * (line.count(R"\x") - line.count(R"\\x") + line.count(R"\\\x"))
for line in lines for line in lines
) )
print(f"answer 1 is {answer_1}")
answer_2 = sum( yield sum(
# needs to wrap in quotes (2 characters) # needs to wrap in quotes (2 characters)
2 2
# needs to escape every \ with an extra \ # needs to escape every \ with an extra \
@ -31,5 +29,4 @@ answer_2 = sum(
# needs to escape every " with an extra \ (including the first and last ones) # needs to escape every " with an extra \ (including the first and last ones)
+ line.count('"') + line.count('"')
for line in lines for line in lines
) )
print(f"answer 2 is {answer_2}")

View File

@ -1,27 +1,28 @@
import itertools import itertools
import sys
from collections import defaultdict from collections import defaultdict
from typing import cast from typing import Any, Iterator, cast
import parse # type: ignore import parse # type: ignore
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
distances: dict[str, dict[str, int]] = defaultdict(dict)
for line in lines: class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
distances: dict[str, dict[str, int]] = defaultdict(dict)
for line in lines:
origin, destination, length = cast( origin, destination, length = cast(
tuple[str, str, int], tuple[str, str, int],
parse.parse("{} to {} = {:d}", line), # type: ignore parse.parse("{} to {} = {:d}", line), # type: ignore
) )
distances[origin][destination] = distances[destination][origin] = length distances[origin][destination] = distances[destination][origin] = length
distance_of_routes = { distance_of_routes = {
route: sum(distances[o][d] for o, d in zip(route[:-1], route[1:])) route: sum(distances[o][d] for o, d in zip(route[:-1], route[1:]))
for route in map(tuple, itertools.permutations(distances)) for route in map(tuple, itertools.permutations(distances))
} }
answer_1 = min(distance_of_routes.values()) yield min(distance_of_routes.values())
print(f"answer 1 is {answer_1}") yield max(distance_of_routes.values())
answer_2 = max(distance_of_routes.values())
print(f"answer 2 is {answer_2}")

View File

@ -1,14 +1,17 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
values = [int(line) for line in lines]
# part 1 class Solver(BaseSolver):
answer_1 = sum(v2 > v1 for v1, v2 in zip(values[:-1], values[1:])) def solve(self, input: str) -> Iterator[Any]:
print(f"answer 1 is {answer_1}") lines = input.splitlines()
# part 2 values = [int(line) for line in lines]
runnings = [sum(values[i : i + 3]) for i in range(len(values) - 2)]
answer_2 = sum(v2 > v1 for v1, v2 in zip(runnings[:-1], runnings[1:])) # part 1
print(f"answer 2 is {answer_2}") yield sum(v2 > v1 for v1, v2 in zip(values[:-1], values[1:]))
# part 2
runnings = [sum(values[i : i + 3]) for i in range(len(values) - 2)]
yield sum(v2 > v1 for v1, v2 in zip(runnings[:-1], runnings[1:]))

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,17 +1,20 @@
import sys
from math import prod from math import prod
from typing import Literal, TypeAlias, cast from typing import Any, Iterator, Literal, TypeAlias, cast
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
Command: TypeAlias = Literal["forward", "up", "down"] Command: TypeAlias = Literal["forward", "up", "down"]
commands: list[tuple[Command, int]] = [
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
commands: list[tuple[Command, int]] = [
(cast(Command, (p := line.split())[0]), int(p[1])) for line in lines (cast(Command, (p := line.split())[0]), int(p[1])) for line in lines
] ]
def depth_and_position(use_aim: bool):
def depth_and_position(use_aim: bool):
aim, pos, depth = 0, 0, 0 aim, pos, depth = 0, 0, 0
for command, value in commands: for command, value in commands:
d_depth = 0 d_depth = 0
@ -31,11 +34,5 @@ def depth_and_position(use_aim: bool):
return depth, pos return depth, pos
yield prod(depth_and_position(False))
# part 1 yield prod(depth_and_position(True))
answer_1 = prod(depth_and_position(False))
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = prod(depth_and_position(True))
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,6 +1,7 @@
import sys
from collections import Counter from collections import Counter
from typing import Literal from typing import Any, Iterator, Literal
from ..base import BaseSolver
def generator_rating( def generator_rating(
@ -20,20 +21,23 @@ def generator_rating(
return values[0] return values[0]
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
# part 1
# part 1 most_and_least_common = [
most_and_least_common = [ tuple(
tuple(Counter(line[col] for line in lines).most_common(2)[m][0] for m in range(2)) Counter(line[col] for line in lines).most_common(2)[m][0]
for m in range(2)
)
for col in range(len(lines[0])) for col in range(len(lines[0]))
] ]
gamma_rate = int("".join(most for most, _ in most_and_least_common), base=2) gamma_rate = int("".join(most for most, _ in most_and_least_common), base=2)
epsilon_rate = int("".join(least for _, least in most_and_least_common), base=2) epsilon_rate = int("".join(least for _, least in most_and_least_common), base=2)
print(f"answer 1 is {gamma_rate * epsilon_rate}") yield gamma_rate * epsilon_rate
# part 2 # part 2
oxygen_generator_rating = int(generator_rating(lines, True, "1"), base=2) oxygen_generator_rating = int(generator_rating(lines, True, "1"), base=2)
co2_scrubber_rating = int(generator_rating(lines, False, "0"), base=2) co2_scrubber_rating = int(generator_rating(lines, False, "0"), base=2)
answer_2 = oxygen_generator_rating * co2_scrubber_rating yield oxygen_generator_rating * co2_scrubber_rating
print(f"answer 2 is {answer_2}")

View File

@ -1,23 +1,28 @@
import sys from typing import Any, Iterator
import numpy as np import numpy as np
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
numbers = [int(c) for c in lines[0].split(",")]
boards = np.asarray( class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
numbers = [int(c) for c in lines[0].split(",")]
boards = np.asarray(
[ [
[[int(c) for c in line.split()] for line in lines[start : start + 5]] [[int(c) for c in line.split()] for line in lines[start : start + 5]]
for start in range(2, len(lines), 6) for start in range(2, len(lines), 6)
] ]
) )
# (round, score) for each board (-1 when not found) # (round, score) for each board (-1 when not found)
winning_rounds: list[tuple[int, int]] = [(-1, -1) for _ in range(len(boards))] winning_rounds: list[tuple[int, int]] = [(-1, -1) for _ in range(len(boards))]
marked = np.zeros_like(boards, dtype=bool) marked = np.zeros_like(boards, dtype=bool)
for round, number in enumerate(numbers): for round, number in enumerate(numbers):
# mark boards # mark boards
marked[boards == number] = True marked[boards == number] = True
@ -26,7 +31,9 @@ for round, number in enumerate(numbers):
if winning_rounds[index][0] > 0: if winning_rounds[index][0] > 0:
continue continue
if np.any(np.all(marked[index], axis=0) | np.all(marked[index], axis=1)): if np.any(
np.all(marked[index], axis=0) | np.all(marked[index], axis=1)
):
winning_rounds[index] = ( winning_rounds[index] = (
round, round,
number * int(np.sum(boards[index][~marked[index]])), number * int(np.sum(boards[index][~marked[index]])),
@ -36,10 +43,10 @@ for round, number in enumerate(numbers):
if np.all(marked.all(axis=1) | marked.all(axis=2)): if np.all(marked.all(axis=1) | marked.all(axis=2)):
break break
# part 1 # part 1
(_, score) = min(winning_rounds, key=lambda w: w[0]) (_, score) = min(winning_rounds, key=lambda w: w[0])
print(f"answer 1 is {score}") yield score
# part 2 # part 2
(_, score) = max(winning_rounds, key=lambda w: w[0]) (_, score) = max(winning_rounds, key=lambda w: w[0])
print(f"answer 2 is {score}") yield score

View File

@ -1,10 +1,15 @@
import sys from typing import Any, Iterator
import numpy as np import numpy as np
lines: list[str] = sys.stdin.read().splitlines() from ..base import BaseSolver
sections: list[tuple[tuple[int, int], tuple[int, int]]] = [
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
sections: list[tuple[tuple[int, int], tuple[int, int]]] = [
( (
( (
int(line.split(" -> ")[0].split(",")[0]), int(line.split(" -> ")[0].split(",")[0]),
@ -16,21 +21,19 @@ sections: list[tuple[tuple[int, int], tuple[int, int]]] = [
), ),
) )
for line in lines for line in lines
] ]
np_sections = np.array(sections).reshape(-1, 4) np_sections = np.array(sections).reshape(-1, 4)
x_min, x_max, y_min, y_max = ( x_max, y_max = (
min(np_sections[:, 0].min(), np_sections[:, 2].min()),
max(np_sections[:, 0].max(), np_sections[:, 2].max()), max(np_sections[:, 0].max(), np_sections[:, 2].max()),
min(np_sections[:, 1].min(), np_sections[:, 3].min()),
max(np_sections[:, 1].max(), np_sections[:, 3].max()), max(np_sections[:, 1].max(), np_sections[:, 3].max()),
) )
counts_1 = np.zeros((y_max + 1, x_max + 1), dtype=int) counts_1 = np.zeros((y_max + 1, x_max + 1), dtype=int)
counts_2 = counts_1.copy() counts_2 = counts_1.copy()
for (x1, y1), (x2, y2) in sections: for (x1, y1), (x2, y2) in sections:
x_rng = range(x1, x2 + 1, 1) if x2 >= x1 else range(x1, x2 - 1, -1) x_rng = range(x1, x2 + 1, 1) if x2 >= x1 else range(x1, x2 - 1, -1)
y_rng = range(y1, y2 + 1, 1) if y2 >= y1 else range(y1, y2 - 1, -1) y_rng = range(y1, y2 + 1, 1) if y2 >= y1 else range(y1, y2 - 1, -1)
@ -41,8 +44,5 @@ for (x1, y1), (x2, y2) in sections:
for i, j in zip(y_rng, x_rng): for i, j in zip(y_rng, x_rng):
counts_2[i, j] += 1 counts_2[i, j] += 1
answer_1 = (counts_1 >= 2).sum() yield (counts_1 >= 2).sum()
print(f"answer 1 is {answer_1}") yield (counts_2 >= 2).sum()
answer_2 = (counts_2 >= 2).sum()
print(f"answer 2 is {answer_2}")

View File

@ -1,21 +1,21 @@
import sys from typing import Any, Iterator
values = [int(c) for c in sys.stdin.read().strip().split(",")] from ..base import BaseSolver
days = 256
lanterns = {day: 0 for day in range(days)} class Solver(BaseSolver):
for value in values: def solve(self, input: str) -> Iterator[Any]:
values = [int(c) for c in input.split(",")]
days = 256
lanterns = {day: 0 for day in range(days)}
for value in values:
for day in range(value, days, 7): for day in range(value, days, 7):
lanterns[day] += 1 lanterns[day] += 1
for day in range(days): for day in range(days):
for day2 in range(day + 9, days, 7): for day2 in range(day + 9, days, 7):
lanterns[day2] += lanterns[day] lanterns[day2] += lanterns[day]
# part 1 yield sum(v for k, v in lanterns.items() if k < 80) + len(values)
answer_1 = sum(v for k, v in lanterns.items() if k < 80) + len(values) yield sum(lanterns.values()) + len(values)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = sum(lanterns.values()) + len(values)
print(f"answer 2 is {answer_2}")

View File

@ -1,19 +1,22 @@
import sys from typing import Any, Iterator
positions = [int(c) for c in sys.stdin.read().strip().split(",")] from ..base import BaseSolver
min_position, max_position = min(positions), max(positions)
# part 1 class Solver(BaseSolver):
answer_1 = min( def solve(self, input: str) -> Iterator[Any]:
positions = [int(c) for c in input.split(",")]
min_position, max_position = min(positions), max(positions)
# part 1
yield min(
sum(abs(p - position) for p in positions) sum(abs(p - position) for p in positions)
for position in range(min_position, max_position + 1) for position in range(min_position, max_position + 1)
) )
print(f"answer 1 is {answer_1}")
# part 2 # part 2
answer_2 = min( yield min(
sum(abs(p - position) * (abs(p - position) + 1) // 2 for p in positions) sum(abs(p - position) * (abs(p - position) + 1) // 2 for p in positions)
for position in range(min_position, max_position + 1) for position in range(min_position, max_position + 1)
) )
print(f"answer 2 is {answer_2}")

View File

@ -1,8 +1,7 @@
import itertools import itertools
import os from typing import Any, Iterator
import sys
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
digits = { digits = {
"abcefg": 0, "abcefg": 0,
@ -17,19 +16,23 @@ digits = {
"abcdfg": 9, "abcdfg": 9,
} }
lines = sys.stdin.read().splitlines()
# part 1 class Solver(BaseSolver):
lengths = {len(k) for k, v in digits.items() if v in (1, 4, 7, 8)} def solve(self, input: str) -> Iterator[Any]:
answer_1 = sum( lines = input.splitlines()
len(p) in lengths for line in lines for p in line.split("|")[1].strip().split()
)
print(f"answer 1 is {answer_1}")
# part 2 # part 1
values: list[int] = [] lengths = {len(k) for k, v in digits.items() if v in (1, 4, 7, 8)}
yield sum(
len(p) in lengths
for line in lines
for p in line.split("|")[1].strip().split()
)
for line in lines: # part 2
values: list[int] = []
for line in lines:
parts = line.split("|") parts = line.split("|")
broken_digits = sorted(parts[0].strip().split(), key=len) broken_digits = sorted(parts[0].strip().split(), key=len)
@ -49,7 +52,9 @@ for line in lines:
bd = [u for u in per_length[4][0] if u not in cf] bd = [u for u in per_length[4][0] if u not in cf]
# the 3 digits of length 5 have a, d and g in common # the 3 digits of length 5 have a, d and g in common
adg = [u for u in per_length[5][0] if all(u in pe for pe in per_length[5][1:])] adg = [
u for u in per_length[5][0] if all(u in pe for pe in per_length[5][1:])
]
# we can remove a # we can remove a
dg = [u for u in adg if u != a] dg = [u for u in adg if u != a]
@ -77,11 +82,8 @@ for line in lines:
digit = "".join(sorted(mapping[c] for c in number)) digit = "".join(sorted(mapping[c] for c in number))
value = 10 * value + digits[digit] value = 10 * value + digits[digit]
if VERBOSE: self.logger.info(f"value for '{line}' is {value}")
print(value)
values.append(value) values.append(value)
yield sum(values)
answer_2 = sum(values)
print(f"answer 2 is {answer_2}")

View File

@ -1,18 +1,18 @@
import sys
from math import prod from math import prod
from typing import Any, Iterator
values = [[int(c) for c in row] for row in sys.stdin.read().splitlines()] from ..base import BaseSolver
n_rows, n_cols = len(values), len(values[0])
def neighbors(point: tuple[int, int]): def neighbors(point: tuple[int, int], n_rows: int, n_cols: int):
i, j = point i, j = point
for di, dj in ((-1, 0), (+1, 0), (0, -1), (0, +1)): for di, dj in ((-1, 0), (+1, 0), (0, -1), (0, +1)):
if 0 <= i + di < n_rows and 0 <= j + dj < n_cols: if 0 <= i + di < n_rows and 0 <= j + dj < n_cols:
yield (i + di, j + dj) yield (i + di, j + dj)
def basin(start: tuple[int, int]) -> set[tuple[int, int]]: def basin(values: list[list[int]], start: tuple[int, int]) -> set[tuple[int, int]]:
n_rows, n_cols = len(values), len(values[0])
visited: set[tuple[int, int]] = set() visited: set[tuple[int, int]] = set()
queue = [start] queue = [start]
@ -23,22 +23,25 @@ def basin(start: tuple[int, int]) -> set[tuple[int, int]]:
continue continue
visited.add((i, j)) visited.add((i, j))
queue.extend(neighbors((i, j))) queue.extend(neighbors((i, j), n_rows, n_cols))
return visited return visited
low_points = [ class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
values = [[int(c) for c in row] for row in input.splitlines()]
n_rows, n_cols = len(values), len(values[0])
low_points = [
(i, j) (i, j)
for i in range(n_rows) for i in range(n_rows)
for j in range(n_cols) for j in range(n_cols)
if all(values[ti][tj] > values[i][j] for ti, tj in neighbors((i, j))) if all(
] values[ti][tj] > values[i][j]
for ti, tj in neighbors((i, j), n_rows, n_cols)
)
]
# part 1 yield sum(values[i][j] + 1 for i, j in low_points)
answer_1 = sum(values[i][j] + 1 for i, j in low_points) yield prod(sorted(len(basin(values, point)) for point in low_points)[-3:])
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = prod(sorted(len(basin(point)) for point in low_points)[-3:])
print(f"answer 2 is {answer_2}")

View File

@ -1,7 +1,12 @@
import sys from typing import Any, Iterator
blocks = sys.stdin.read().split("\n\n") from ..base import BaseSolver
values = sorted(sum(map(int, block.split())) for block in blocks)
print(f"answer 1 is {values[-1]}")
print(f"answer 2 is {sum(values[-3:])}") class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
blocks = input.split("\n\n")
values = sorted(sum(map(int, block.split())) for block in blocks)
yield values[-1]
yield sum(values[-3:])

View File

@ -1,13 +1,16 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
cycle = 1
x = 1
values = {cycle: x} class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
for line in lines: cycle, x = 1, 1
values = {cycle: x}
for line in lines:
cycle += 1 cycle += 1
if line == "noop": if line == "noop":
@ -22,17 +25,19 @@ for line in lines:
values[cycle] = x values[cycle] = x
answer_1 = sum(c * values[c] for c in range(20, max(values.keys()) + 1, 40)) answer_1 = sum(c * values[c] for c in range(20, max(values.keys()) + 1, 40))
print(f"answer 1 is {answer_1}") yield answer_1
yield (
for i in range(6): "\n"
for j in range(40): + "\n".join(
v = values[1 + i * 40 + j] "".join(
"#"
if j >= v - 1 and j <= v + 1: if j >= (v := values[1 + i * 40 + j]) - 1 and j <= v + 1
print("#", end="") else "."
else: for j in range(40)
print(".", end="") )
for i in range(6)
print() )
+ "\n"
)

View File

@ -1,7 +1,8 @@
import copy import copy
import sys
from functools import reduce from functools import reduce
from typing import Callable, Final, Mapping, Sequence from typing import Any, Callable, Final, Iterator, Mapping, Sequence
from ..base import BaseSolver
class Monkey: class Monkey:
@ -119,24 +120,28 @@ def monkey_business(inspects: dict[Monkey, int]) -> int:
return sorted_levels[-2] * sorted_levels[-1] return sorted_levels[-2] * sorted_levels[-1]
monkeys = [parse_monkey(block.splitlines()) for block in sys.stdin.read().split("\n\n")] class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
monkeys = [parse_monkey(block.splitlines()) for block in input.split("\n\n")]
# case 1: we simply divide the worry by 3 after applying the monkey worry operation # case 1: we simply divide the worry by 3 after applying the monkey worry operation
answer_1 = monkey_business( yield monkey_business(
run(copy.deepcopy(monkeys), 20, me_worry_fn=lambda w: w // 3) run(copy.deepcopy(monkeys), 20, me_worry_fn=lambda w: w // 3)
) )
print(f"answer 1 is {answer_1}")
# case 2: to keep reasonable level values, we can use a modulo operation, we need to # case 2: to keep reasonable level values, we can use a modulo operation, we need to
# use the product of all "divisible by" test so that the test remains valid # use the product of all "divisible by" test so that the test remains valid
# #
# (a + b) % c == ((a % c) + (b % c)) % c --- this would work for a single test value # (a + b) % c == ((a % c) + (b % c)) % c --- this would work for a single test value
# #
# (a + b) % c == ((a % d) + (b % d)) % c --- if d is a multiple of c, which is why here # (a + b) % c == ((a % d) + (b % d)) % c --- if d is a multiple of c, which is why here
# we use the product of all test value # we use the product of all test value
# #
total_test_value = reduce(lambda w, m: w * m.test_value, monkeys, 1) total_test_value = reduce(lambda w, m: w * m.test_value, monkeys, 1)
answer_2 = monkey_business( yield monkey_business(
run(copy.deepcopy(monkeys), 10_000, me_worry_fn=lambda w: w % total_test_value) run(
) copy.deepcopy(monkeys),
print(f"answer 2 is {answer_2}") 10_000,
me_worry_fn=lambda w: w % total_test_value,
)
)

View File

@ -1,6 +1,7 @@
import heapq import heapq
import sys from typing import Any, Callable, Iterator, TypeVar
from typing import Callable, Iterator, TypeVar
from ..base import BaseSolver
Node = TypeVar("Node") Node = TypeVar("Node")
@ -68,30 +69,6 @@ def make_path(parents: dict[Node, Node], start: Node, end: Node) -> list[Node] |
return list(reversed(path)) return list(reversed(path))
def print_path(path: list[tuple[int, int]], n_rows: int, n_cols: int) -> None:
end = path[-1]
graph = [["." for _c in range(n_cols)] for _r in range(n_rows)]
graph[end[0]][end[1]] = "E"
for i in range(0, len(path) - 1):
cr, cc = path[i]
nr, nc = path[i + 1]
if cr == nr and nc == cc - 1:
graph[cr][cc] = "<"
elif cr == nr and nc == cc + 1:
graph[cr][cc] = ">"
elif cr == nr - 1 and nc == cc:
graph[cr][cc] = "v"
elif cr == nr + 1 and nc == cc:
graph[cr][cc] = "^"
else:
assert False, "{} -> {} infeasible".format(path[i], path[i + 1])
print("\n".join("".join(row) for row in graph))
def neighbors( def neighbors(
grid: list[list[int]], node: tuple[int, int], up: bool grid: list[list[int]], node: tuple[int, int], up: bool
) -> Iterator[tuple[int, int]]: ) -> Iterator[tuple[int, int]]:
@ -118,17 +95,44 @@ def neighbors(
# === main code === # === main code ===
lines = sys.stdin.read().splitlines()
grid = [[ord(cell) - ord("a") for cell in line] for line in lines] class Solver(BaseSolver):
def print_path(self, path: list[tuple[int, int]], n_rows: int, n_cols: int) -> None:
end = path[-1]
start: tuple[int, int] | None = None graph = [["." for _c in range(n_cols)] for _r in range(n_rows)]
end: tuple[int, int] | None = None graph[end[0]][end[1]] = "E"
# for part 2 for i in range(0, len(path) - 1):
start_s: list[tuple[int, int]] = [] cr, cc = path[i]
nr, nc = path[i + 1]
for i_row, row in enumerate(grid): if cr == nr and nc == cc - 1:
graph[cr][cc] = "<"
elif cr == nr and nc == cc + 1:
graph[cr][cc] = ">"
elif cr == nr - 1 and nc == cc:
graph[cr][cc] = "v"
elif cr == nr + 1 and nc == cc:
graph[cr][cc] = "^"
else:
assert False, "{} -> {} infeasible".format(path[i], path[i + 1])
for row in graph:
self.logger.info("".join(row))
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
grid = [[ord(cell) - ord("a") for cell in line] for line in lines]
start: tuple[int, int] | None = None
end: tuple[int, int] | None = None
# for part 2
start_s: list[tuple[int, int]] = []
for i_row, row in enumerate(grid):
for i_col, col in enumerate(row): for i_col, col in enumerate(row):
if chr(col + ord("a")) == "S": if chr(col + ord("a")) == "S":
start = (i_row, i_col) start = (i_row, i_col)
@ -138,26 +142,27 @@ for i_row, row in enumerate(grid):
elif col == 0: elif col == 0:
start_s.append((i_row, i_col)) start_s.append((i_row, i_col))
assert start is not None assert start is not None
assert end is not None assert end is not None
# fix values # fix values
grid[start[0]][start[1]] = 0 grid[start[0]][start[1]] = 0
grid[end[0]][end[1]] = ord("z") - ord("a") grid[end[0]][end[1]] = ord("z") - ord("a")
lengths_1, parents_1 = dijkstra(
start=start,
neighbors=lambda n: neighbors(grid, n, True),
cost=lambda lhs, rhs: 1,
)
path_1 = make_path(parents_1, start, end)
assert path_1 is not None
lengths_1, parents_1 = dijkstra( self.print_path(path_1, n_rows=len(grid), n_cols=len(grid[0]))
start=start, neighbors=lambda n: neighbors(grid, n, True), cost=lambda lhs, rhs: 1 yield lengths_1[end] - 1
)
path_1 = make_path(parents_1, start, end)
assert path_1 is not None
print_path(path_1, n_rows=len(grid), n_cols=len(grid[0])) lengths_2, _ = dijkstra(
start=end,
print(f"answer 1 is {lengths_1[end] - 1}") neighbors=lambda n: neighbors(grid, n, False),
cost=lambda lhs, rhs: 1,
lengths_2, parents_2 = dijkstra( )
start=end, neighbors=lambda n: neighbors(grid, n, False), cost=lambda lhs, rhs: 1 yield min(lengths_2.get(start, float("inf")) for start in start_s)
)
answer_2 = min(lengths_2.get(start, float("inf")) for start in start_s)
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,8 @@
import json import json
import sys
from functools import cmp_to_key from functools import cmp_to_key
from typing import TypeAlias, cast from typing import Any, Iterator, TypeAlias, cast
blocks = sys.stdin.read().strip().split("\n\n") from ..base import BaseSolver
pairs = [tuple(json.loads(p) for p in block.split("\n")) for block in blocks]
Packet: TypeAlias = list[int | list["Packet"]] Packet: TypeAlias = list[int | list["Packet"]]
@ -28,14 +25,18 @@ def compare(lhs: Packet, rhs: Packet) -> int:
return len(rhs) - len(lhs) return len(rhs) - len(lhs)
answer_1 = sum(i + 1 for i, (lhs, rhs) in enumerate(pairs) if compare(lhs, rhs) > 0) class Solver(BaseSolver):
print(f"answer_1 is {answer_1}") def solve(self, input: str) -> Iterator[Any]:
blocks = input.split("\n\n")
pairs = [tuple(json.loads(p) for p in block.split("\n")) for block in blocks]
dividers = [[[2]], [[6]]] yield sum(i + 1 for i, (lhs, rhs) in enumerate(pairs) if compare(lhs, rhs) > 0)
packets = [packet for packets in pairs for packet in packets] dividers = [[[2]], [[6]]]
packets.extend(dividers)
packets = list(reversed(sorted(packets, key=cmp_to_key(compare))))
d_index = [packets.index(d) + 1 for d in dividers] packets = [packet for packets in pairs for packet in packets]
print(f"answer 2 is {d_index[0] * d_index[1]}") packets.extend(dividers)
packets = list(reversed(sorted(packets, key=cmp_to_key(compare))))
d_index = [packets.index(d) + 1 for d in dividers]
yield d_index[0] * d_index[1]

View File

@ -1,6 +1,7 @@
import sys
from enum import Enum, auto from enum import Enum, auto
from typing import Callable, cast from typing import Any, Callable, Iterator, cast
from ..base import BaseSolver
class Cell(Enum): class Cell(Enum):
@ -12,26 +13,6 @@ class Cell(Enum):
return {Cell.AIR: ".", Cell.ROCK: "#", Cell.SAND: "O"}[self] return {Cell.AIR: ".", Cell.ROCK: "#", Cell.SAND: "O"}[self]
def print_blocks(blocks: dict[tuple[int, int], Cell]):
"""
Print the given set of blocks on a grid.
Args:
blocks: Set of blocks to print.
"""
x_min, y_min, x_max, y_max = (
min(x for x, _ in blocks),
0,
max(x for x, _ in blocks),
max(y for _, y in blocks),
)
for y in range(y_min, y_max + 1):
print(
"".join(str(blocks.get((x, y), Cell.AIR)) for x in range(x_min, x_max + 1))
)
def flow( def flow(
blocks: dict[tuple[int, int], Cell], blocks: dict[tuple[int, int], Cell],
stop_fn: Callable[[int, int], bool], stop_fn: Callable[[int, int], bool],
@ -84,21 +65,46 @@ def flow(
# === inputs === # === inputs ===
lines = sys.stdin.read().splitlines()
paths: list[list[tuple[int, int]]] = [] class Solver(BaseSolver):
for line in lines: def print_blocks(self, blocks: dict[tuple[int, int], Cell]):
"""
Print the given set of blocks on a grid.
Args:
blocks: Set of blocks to print.
"""
x_min, y_min, x_max, y_max = (
min(x for x, _ in blocks),
0,
max(x for x, _ in blocks),
max(y for _, y in blocks),
)
for y in range(y_min, y_max + 1):
self.logger.info(
"".join(
str(blocks.get((x, y), Cell.AIR)) for x in range(x_min, x_max + 1)
)
)
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
paths: list[list[tuple[int, int]]] = []
for line in lines:
parts = line.split(" -> ") parts = line.split(" -> ")
paths.append( paths.append(
[ [
cast(tuple[int, int], tuple(int(c.strip()) for c in part.split(","))) cast(
tuple[int, int], tuple(int(c.strip()) for c in part.split(","))
)
for part in parts for part in parts
] ]
) )
blocks: dict[tuple[int, int], Cell] = {}
blocks: dict[tuple[int, int], Cell] = {} for path in paths:
for path in paths:
for start, end in zip(path[:-1], path[1:]): for start, end in zip(path[:-1], path[1:]):
x_start = min(start[0], end[0]) x_start = min(start[0], end[0])
x_end = max(start[0], end[0]) + 1 x_end = max(start[0], end[0]) + 1
@ -109,32 +115,25 @@ for path in paths:
for y in range(y_start, y_end): for y in range(y_start, y_end):
blocks[x, y] = Cell.ROCK blocks[x, y] = Cell.ROCK
print_blocks(blocks) self.print_blocks(blocks)
print()
x_min, y_min, x_max, y_max = ( y_max = max(y for _, y in blocks)
min(x for x, _ in blocks),
0,
max(x for x, _ in blocks),
max(y for _, y in blocks),
)
# === part 1 === # === part 1 ===
blocks_1 = flow( blocks_1 = flow(
blocks.copy(), stop_fn=lambda x, y: y > y_max, fill_fn=lambda x, y: Cell.AIR blocks.copy(), stop_fn=lambda x, y: y > y_max, fill_fn=lambda x, y: Cell.AIR
) )
print_blocks(blocks_1) self.print_blocks(blocks_1)
print(f"answer 1 is {sum(v == Cell.SAND for v in blocks_1.values())}") yield sum(v == Cell.SAND for v in blocks_1.values())
print()
# === part 2 === # === part 2 ===
blocks_2 = flow( blocks_2 = flow(
blocks.copy(), blocks.copy(),
stop_fn=lambda x, y: x == 500 and y == 0, stop_fn=lambda x, y: x == 500 and y == 0,
fill_fn=lambda x, y: Cell.AIR if y < y_max + 2 else Cell.ROCK, fill_fn=lambda x, y: Cell.AIR if y < y_max + 2 else Cell.ROCK,
) )
blocks_2[500, 0] = Cell.SAND blocks_2[500, 0] = Cell.SAND
print_blocks(blocks_2) self.print_blocks(blocks_2)
print(f"answer 2 is {sum(v == Cell.SAND for v in blocks_2.values())}") yield sum(v == Cell.SAND for v in blocks_2.values())

View File

@ -1,12 +1,17 @@
import sys import itertools as it
from typing import Any from typing import Any, Iterator
import numpy as np import numpy as np
import parse # type: ignore import parse # type: ignore
from numpy.typing import NDArray from numpy.typing import NDArray
from ..base import BaseSolver
def part1(sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], row: int) -> int:
class Solver(BaseSolver):
def part1(
self, sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], row: int
) -> int:
no_beacons_row_l: list[NDArray[np.floating[Any]]] = [] no_beacons_row_l: list[NDArray[np.floating[Any]]] = []
for (sx, sy), (bx, by) in sensor_to_beacon.items(): for (sx, sy), (bx, by) in sensor_to_beacon.items():
@ -16,17 +21,14 @@ def part1(sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], row: int) ->
no_beacons_row_l.append(sx + np.arange(0, d - abs(sy - row) + 1)) # type: ignore no_beacons_row_l.append(sx + np.arange(0, d - abs(sy - row) + 1)) # type: ignore
beacons_at_row = set(bx for (bx, by) in sensor_to_beacon.values() if by == row) beacons_at_row = set(bx for (bx, by) in sensor_to_beacon.values() if by == row)
no_beacons_row = set(np.concatenate(no_beacons_row_l)).difference(beacons_at_row) # type: ignore no_beacons_row = set(it.chain(*no_beacons_row_l)).difference(beacons_at_row) # type: ignore
return len(no_beacons_row) return len(no_beacons_row)
def part2_intervals(
def part2_intervals( self, sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int ) -> tuple[int, int, int]:
) -> tuple[int, int, int]: for y in self.progress.wrap(range(xy_max + 1)):
from tqdm import trange
for y in trange(xy_max + 1):
its: list[tuple[int, int]] = [] its: list[tuple[int, int]] = []
for (sx, sy), (bx, by) in sensor_to_beacon.items(): for (sx, sy), (bx, by) in sensor_to_beacon.items():
d = abs(sx - bx) + abs(sy - by) d = abs(sx - bx) + abs(sy - by)
@ -46,10 +48,9 @@ def part2_intervals(
return (0, 0, 0) return (0, 0, 0)
def part2_cplex(
def part2_cplex( self, sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int ) -> tuple[int, int, int]:
) -> tuple[int, int, int]:
from docplex.mp.model import Model from docplex.mp.model import Model
m = Model() m = Model()
@ -58,7 +59,10 @@ def part2_cplex(
for (sx, sy), (bx, by) in sensor_to_beacon.items(): for (sx, sy), (bx, by) in sensor_to_beacon.items():
d = abs(sx - bx) + abs(sy - by) d = abs(sx - bx) + abs(sy - by)
m.add_constraint(m.abs(x - sx) + m.abs(y - sy) >= d + 1, ctname=f"ct_{sx}_{sy}") # type: ignore m.add_constraint(
m.abs(x - sx) + m.abs(y - sy) >= d + 1, # type: ignore
ctname=f"ct_{sx}_{sy}",
)
m.set_objective("min", x + y) m.set_objective("min", x + y)
@ -69,22 +73,23 @@ def part2_cplex(
vy = int(s.get_value(y)) vy = int(s.get_value(y))
return vx, vy, 4_000_000 * vx + vy return vx, vy, 4_000_000 * vx + vy
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
lines = sys.stdin.read().splitlines() sensor_to_beacon: dict[tuple[int, int], tuple[int, int]] = {}
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]] = {} for line in lines:
for line in lines:
r: dict[str, str] = parse.parse( # type: ignore r: dict[str, str] = parse.parse( # type: ignore
"Sensor at x={sx}, y={sy}: closest beacon is at x={bx}, y={by}", line "Sensor at x={sx}, y={sy}: closest beacon is at x={bx}, y={by}", line
) )
sensor_to_beacon[int(r["sx"]), int(r["sy"])] = (int(r["bx"]), int(r["by"])) sensor_to_beacon[int(r["sx"]), int(r["sy"])] = (int(r["bx"]), int(r["by"]))
xy_max = 4_000_000 if max(sensor_to_beacon) > (1_000, 0) else 20 xy_max = 4_000_000 if max(sensor_to_beacon) > (1_000, 0) else 20
row = 2_000_000 if max(sensor_to_beacon) > (1_000, 0) else 10 row = 2_000_000 if max(sensor_to_beacon) > (1_000, 0) else 10
print(f"answer 1 is {part1(sensor_to_beacon, row)}") yield self.part1(sensor_to_beacon, row)
# x, y, a2 = part2_cplex(sensor_to_beacon, xy_max) # x, y, a2 = part2_cplex(sensor_to_beacon, xy_max)
x, y, a2 = part2_intervals(sensor_to_beacon, xy_max) x, y, a2 = self.part2_intervals(sensor_to_beacon, xy_max)
print(f"answer 2 is {a2} (x={x}, y={y})") self.logger.info(f"answer 2 is {a2} (x={x}, y={y})")
yield a2

View File

@ -3,12 +3,13 @@ from __future__ import annotations
import heapq import heapq
import itertools import itertools
import re import re
import sys
from collections import defaultdict from collections import defaultdict
from typing import FrozenSet, NamedTuple from typing import Any, FrozenSet, Iterator, NamedTuple
from tqdm import tqdm from tqdm import tqdm
from ..base import BaseSolver
class Pipe(NamedTuple): class Pipe(NamedTuple):
name: str name: str
@ -36,8 +37,8 @@ def breadth_first_search(pipes: dict[str, Pipe], pipe: Pipe) -> dict[Pipe, int]:
Runs a BFS from the given pipe and return the shortest distance (in term of hops) Runs a BFS from the given pipe and return the shortest distance (in term of hops)
to all other pipes. to all other pipes.
""" """
queue = [(0, pipe_1)] queue = [(0, pipe)]
visited = set() visited: set[Pipe] = set()
distances: dict[Pipe, int] = {} distances: dict[Pipe, int] = {}
while len(distances) < len(pipes): while len(distances) < len(pipes):
@ -122,11 +123,12 @@ def part_2(
# === MAIN === # === MAIN ===
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
pipes: dict[str, Pipe] = {}
pipes: dict[str, Pipe] = {} for line in lines:
for line in lines:
r = re.match( r = re.match(
R"Valve ([A-Z]+) has flow rate=([0-9]+); tunnels? leads? to valves? (.+)", R"Valve ([A-Z]+) has flow rate=([0-9]+); tunnels? leads? to valves? (.+)",
line, line,
@ -137,9 +139,9 @@ for line in lines:
pipes[g[0]] = Pipe(g[0], int(g[1]), g[2].split(", ")) pipes[g[0]] = Pipe(g[0], int(g[1]), g[2].split(", "))
# compute distances from one valve to any other # compute distances from one valve to any other
distances: dict[tuple[Pipe, Pipe], int] = {} distances: dict[tuple[Pipe, Pipe], int] = {}
for pipe_1 in pipes.values(): for pipe_1 in pipes.values():
distances.update( distances.update(
{ {
(pipe_1, pipe_2): distance (pipe_1, pipe_2): distance
@ -147,12 +149,11 @@ for pipe_1 in pipes.values():
} }
) )
# valves with flow # valves with flow
relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0) relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0)
# 1651, 1653
yield part_1(pipes["AA"], 30, distances, relevant_pipes)
# 1651, 1653 # 1707, 2223
print(part_1(pipes["AA"], 30, distances, relevant_pipes)) yield part_2(pipes["AA"], 26, distances, relevant_pipes)
# 1707, 2223
print(part_2(pipes["AA"], 26, distances, relevant_pipes))

View File

@ -1,12 +1,16 @@
import sys from typing import Any, Iterator, Sequence, TypeAlias, TypeVar
from typing import Sequence, TypeVar
import numpy as np import numpy as np
from numpy.typing import NDArray
from ..base import BaseSolver
T = TypeVar("T") T = TypeVar("T")
Tower: TypeAlias = NDArray[np.bool]
def print_tower(tower: np.ndarray, out: str = "#"):
def print_tower(tower: Tower, out: str = "#"):
print("-" * (tower.shape[1] + 2)) print("-" * (tower.shape[1] + 2))
non_empty = False non_empty = False
for row in reversed(range(1, tower.shape[0])): for row in reversed(range(1, tower.shape[0])):
@ -17,7 +21,7 @@ def print_tower(tower: np.ndarray, out: str = "#"):
print("+" + "-" * tower.shape[1] + "+") print("+" + "-" * tower.shape[1] + "+")
def tower_height(tower: np.ndarray) -> int: def tower_height(tower: Tower) -> int:
return int(tower.shape[0] - tower[::-1, :].argmax(axis=0).min() - 1) return int(tower.shape[0] - tower[::-1, :].argmax(axis=0).min() - 1)
@ -45,8 +49,8 @@ def build_tower(
n_rocks: int, n_rocks: int,
jets: str, jets: str,
early_stop: bool = False, early_stop: bool = False,
init: np.ndarray = np.ones(WIDTH, dtype=bool), init: Tower = np.ones(WIDTH, dtype=bool),
) -> tuple[np.ndarray, int, int, dict[int, int]]: ) -> tuple[Tower, int, int, dict[int, int]]:
tower = EMPTY_BLOCKS.copy() tower = EMPTY_BLOCKS.copy()
tower[0, :] = init tower[0, :] = init
@ -95,26 +99,24 @@ def build_tower(
return tower, rock_count, done_at.get((i_rock, i_jet), -1), heights return tower, rock_count, done_at.get((i_rock, i_jet), -1), heights
line = sys.stdin.read().strip() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
tower, *_ = build_tower(2022, input)
yield tower_height(tower)
tower, *_ = build_tower(2022, line) TOTAL_ROCKS = 1_000_000_000_000
answer_1 = tower_height(tower) _tower_1, n_rocks_1, prev_1, heights_1 = build_tower(TOTAL_ROCKS, input, True)
print(f"answer 1 is {answer_1}") assert prev_1 > 0
TOTAL_ROCKS = 1_000_000_000_000 # 2767 1513
tower_1, n_rocks_1, prev_1, heights_1 = build_tower(TOTAL_ROCKS, line, True) remaining_rocks = TOTAL_ROCKS - n_rocks_1
assert prev_1 > 0 n_repeat_rocks = n_rocks_1 - prev_1
n_repeat_towers = remaining_rocks // n_repeat_rocks
# 2767 1513 base_height = heights_1[prev_1]
remaining_rocks = TOTAL_ROCKS - n_rocks_1 repeat_height = heights_1[prev_1 + n_repeat_rocks - 1] - heights_1[prev_1]
n_repeat_rocks = n_rocks_1 - prev_1 remaining_height = (
n_repeat_towers = remaining_rocks // n_repeat_rocks
base_height = heights_1[prev_1]
repeat_height = heights_1[prev_1 + n_repeat_rocks - 1] - heights_1[prev_1]
remaining_height = (
heights_1[prev_1 + remaining_rocks % n_repeat_rocks] - heights_1[prev_1] heights_1[prev_1 + remaining_rocks % n_repeat_rocks] - heights_1[prev_1]
) )
answer_2 = base_height + (n_repeat_towers + 1) * repeat_height + remaining_height yield base_height + (n_repeat_towers + 1) * repeat_height + remaining_height
print(f"answer 2 is {answer_2}")

View File

@ -1,33 +1,38 @@
import sys from typing import Any, Iterator
import numpy as np import numpy as np
xyz = np.asarray( from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
xyz = np.asarray(
[ [
tuple(int(x) for x in row.split(",")) # type: ignore tuple(int(x) for x in row.split(",")) # type: ignore
for row in sys.stdin.read().splitlines() for row in input.splitlines()
] ]
) )
xyz = xyz - xyz.min(axis=0) + 1 xyz = xyz - xyz.min(axis=0) + 1
cubes = np.zeros(xyz.max(axis=0) + 3, dtype=bool) cubes = np.zeros(xyz.max(axis=0) + 3, dtype=bool)
cubes[xyz[:, 0], xyz[:, 1], xyz[:, 2]] = True cubes[xyz[:, 0], xyz[:, 1], xyz[:, 2]] = True
n_dims = len(cubes.shape) faces = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)]
faces = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)] yield sum(
1
for x, y, z in xyz
for dx, dy, dz in faces
if not cubes[x + dx, y + dy, z + dz]
)
answer_1 = sum( visited = np.zeros_like(cubes, dtype=bool)
1 for x, y, z in xyz for dx, dy, dz in faces if not cubes[x + dx, y + dy, z + dz] queue = [(0, 0, 0)]
)
print(f"answer 1 is {answer_1}")
visited = np.zeros_like(cubes, dtype=bool) n_faces = 0
queue = [(0, 0, 0)] while queue:
n_faces = 0
while queue:
x, y, z = queue.pop(0) x, y, z = queue.pop(0)
if visited[x, y, z]: if visited[x, y, z]:
@ -37,7 +42,9 @@ while queue:
for dx, dy, dz in faces: for dx, dy, dz in faces:
nx, ny, nz = x + dx, y + dy, z + dz nx, ny, nz = x + dx, y + dy, z + dz
if not all(n >= 0 and n < cubes.shape[i] for i, n in enumerate((nx, ny, nz))): if not all(
n >= 0 and n < cubes.shape[i] for i, n in enumerate((nx, ny, nz))
):
continue continue
if visited[nx, ny, nz]: if visited[nx, ny, nz]:
@ -47,4 +54,5 @@ while queue:
n_faces += 1 n_faces += 1
else: else:
queue.append((nx, ny, nz)) queue.append((nx, ny, nz))
print(f"answer 2 is {n_faces}")
yield n_faces

View File

@ -1,10 +1,11 @@
import sys from typing import Any, Iterator, Literal
from typing import Any, Literal
import numpy as np import numpy as np
import parse # pyright: ignore[reportMissingTypeStubs] import parse # pyright: ignore[reportMissingTypeStubs]
from numpy.typing import NDArray from numpy.typing import NDArray
from ..base import BaseSolver
Reagent = Literal["ore", "clay", "obsidian", "geode"] Reagent = Literal["ore", "clay", "obsidian", "geode"]
REAGENTS: tuple[Reagent, ...] = ( REAGENTS: tuple[Reagent, ...] = (
"ore", "ore",
@ -62,29 +63,6 @@ def dominates(lhs: State, rhs: State):
) )
lines = sys.stdin.read().splitlines()
blueprints: list[dict[Reagent, IntOfReagent]] = []
for line in lines:
r: list[int] = parse.parse( # type: ignore
"Blueprint {}: "
"Each ore robot costs {:d} ore. "
"Each clay robot costs {:d} ore. "
"Each obsidian robot costs {:d} ore and {:d} clay. "
"Each geode robot costs {:d} ore and {:d} obsidian.",
line,
)
blueprints.append(
{
"ore": {"ore": r[1]},
"clay": {"ore": r[2]},
"obsidian": {"ore": r[3], "clay": r[4]},
"geode": {"ore": r[5], "obsidian": r[6]},
}
)
def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int: def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
# since we can only build one robot per time, we do not need more than X robots # since we can only build one robot per time, we do not need more than X robots
# of type K where X is the maximum number of K required among all robots, e.g., # of type K where X is the maximum number of K required among all robots, e.g.,
@ -173,11 +151,31 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
return max(state.reagents["geode"] for state in state_after_t[max_time]) return max(state.reagents["geode"] for state in state_after_t[max_time])
answer_1 = sum( class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
blueprints: list[dict[Reagent, IntOfReagent]] = []
for line in input.splitlines():
r: list[int] = parse.parse( # type: ignore
"Blueprint {}: "
"Each ore robot costs {:d} ore. "
"Each clay robot costs {:d} ore. "
"Each obsidian robot costs {:d} ore and {:d} clay. "
"Each geode robot costs {:d} ore and {:d} obsidian.",
line,
)
blueprints.append(
{
"ore": {"ore": r[1]},
"clay": {"ore": r[2]},
"obsidian": {"ore": r[3], "clay": r[4]},
"geode": {"ore": r[5], "obsidian": r[6]},
}
)
yield sum(
(i_blueprint + 1) * run(blueprint, 24) (i_blueprint + 1) * run(blueprint, 24)
for i_blueprint, blueprint in enumerate(blueprints) for i_blueprint, blueprint in enumerate(blueprints)
) )
print(f"answer 1 is {answer_1}")
answer_2 = run(blueprints[0], 32) * run(blueprints[1], 32) * run(blueprints[2], 32) yield (run(blueprints[0], 32) * run(blueprints[1], 32) * run(blueprints[2], 32))
print(f"answer 2 is {answer_2}")

View File

@ -1,4 +1,6 @@
import sys from typing import Any, Iterator
from ..base import BaseSolver
def score_1(ux: int, vx: int) -> int: def score_1(ux: int, vx: int) -> int:
@ -33,21 +35,23 @@ def score_2(ux: int, vx: int) -> int:
return (ux + vx - 1) % 3 + 1 + vx * 3 return (ux + vx - 1) % 3 + 1 + vx * 3
lines = sys.stdin.readlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
# the solution relies on replacing rock / paper / scissor by values 0 / 1 / 2 and using # the solution relies on replacing rock / paper / scissor by values 0 / 1 / 2 and using
# modulo-3 arithmetic # modulo-3 arithmetic
# #
# in modulo-3 arithmetic, the winning move is 1 + the opponent move (e.g., winning move # in modulo-3 arithmetic, the winning move is 1 + the opponent move (e.g., winning move
# if opponent plays 0 is 1, or 0 if opponent plays 2 (0 = (2 + 1 % 3))) # if opponent plays 0 is 1, or 0 if opponent plays 2 (0 = (2 + 1 % 3)))
# #
# we read the lines in a Nx2 in array with value 0/1/2 instead of A/B/C or X/Y/Z for # we read the lines in a Nx2 in array with value 0/1/2 instead of A/B/C or X/Y/Z for
# easier manipulation # easier manipulation
values = [(ord(row[0]) - ord("A"), ord(row[2]) - ord("X")) for row in lines] values = [(ord(row[0]) - ord("A"), ord(row[2]) - ord("X")) for row in lines]
# part 1 - 13526 # part 1 - 13526
print(f"answer 1 is {sum(score_1(*v) for v in values)}") yield sum(score_1(*v) for v in values)
# part 2 - 14204 # part 2 - 14204
print(f"answer 2 is {sum(score_2(*v) for v in values)}") yield sum(score_2(*v) for v in values)

View File

@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import sys from typing import Any, Iterator
from ..base import BaseSolver
class Number: class Number:
@ -65,10 +67,9 @@ def decrypt(numbers: list[Number], key: int, rounds: int) -> int:
) )
numbers = [Number(int(x)) for i, x in enumerate(sys.stdin.readlines())] class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
numbers = [Number(int(x)) for x in input.splitlines()]
answer_1 = decrypt(numbers, 1, 1) yield decrypt(numbers, 1, 1)
print(f"answer 1 is {answer_1}") yield decrypt(numbers, 811589153, 10)
answer_2 = decrypt(numbers, 811589153, 10)
print(f"answer 2 is {answer_2}")

View File

@ -1,6 +1,7 @@
import operator import operator
import sys from typing import Any, Callable, Iterator
from typing import Callable
from ..base import BaseSolver
def compute(monkeys: dict[str, int | tuple[str, str, str]], monkey: str) -> int: def compute(monkeys: dict[str, int | tuple[str, str, str]], monkey: str) -> int:
@ -77,13 +78,15 @@ def invert(
return monkeys return monkeys
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
monkeys: dict[str, int | tuple[str, str, str]] = {} monkeys: dict[str, int | tuple[str, str, str]] = {}
op_monkeys: set[str] = set() op_monkeys: set[str] = set()
for line in lines: for line in lines:
parts = line.split(":") parts = line.split(":")
name = parts[0].strip() name = parts[0].strip()
@ -96,12 +99,10 @@ for line in lines:
op_monkeys.add(name) op_monkeys.add(name)
yield compute(monkeys.copy(), "root")
answer_1 = compute(monkeys.copy(), "root") # assume the second operand of 'root' can be computed, and the first one depends on
print(f"answer 1 is {answer_1}") # humn, which is the case is my input and the test input
assert isinstance(monkeys["root"], tuple)
# assume the second operand of 'root' can be computed, and the first one depends on p1, _, p2 = monkeys["root"] # type: ignore
# humn, which is the case is my input and the test input yield compute(invert(monkeys, "humn", compute(monkeys.copy(), p2)), "humn")
p1, _, p2 = monkeys["root"] # type: ignore
answer_2 = compute(invert(monkeys, "humn", compute(monkeys.copy(), p2)), "humn")
print(f"answer 2 is {answer_2}")

View File

@ -1,51 +1,57 @@
import re import re
import sys from typing import Any, Callable, Iterator
from typing import Callable
import numpy as np import numpy as np
from ..base import BaseSolver
VOID, EMPTY, WALL = 0, 1, 2 VOID, EMPTY, WALL = 0, 1, 2
TILE_FROM_CHAR = {" ": VOID, ".": EMPTY, "#": WALL} TILE_FROM_CHAR = {" ": VOID, ".": EMPTY, "#": WALL}
SCORES = {"E": 0, "S": 1, "W": 2, "N": 3} SCORES = {"E": 0, "S": 1, "W": 2, "N": 3}
board_map_s, direction_s = sys.stdin.read().split("\n\n") class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
board_map_s, direction_s = input.split("\n\n")
# board # board
board_lines = board_map_s.splitlines() board_lines = board_map_s.splitlines()
max_line = max(len(line) for line in board_lines) max_line = max(len(line) for line in board_lines)
board = np.array( board = np.array(
[ [
[TILE_FROM_CHAR[c] for c in row] + [VOID] * (max_line - len(row)) [TILE_FROM_CHAR[c] for c in row] + [VOID] * (max_line - len(row))
for row in board_map_s.splitlines() for row in board_map_s.splitlines()
] ]
) )
directions = [ directions = [
int(p1) if p2 else p1 for p1, p2 in re.findall(R"(([0-9])+|L|R)", direction_s) int(p1) if p2 else p1
] for p1, p2 in re.findall(R"(([0-9])+|L|R)", direction_s)
]
# find on each row and column the first and last non-void
row_first_non_void = np.argmax(board != VOID, axis=1)
row_last_non_void = (
board.shape[1] - np.argmax(board[:, ::-1] != VOID, axis=1) - 1
)
col_first_non_void = np.argmax(board != VOID, axis=0)
col_last_non_void = (
board.shape[0] - np.argmax(board[::-1, :] != VOID, axis=0) - 1
)
# find on each row and column the first and last non-void faces = np.zeros_like(board)
row_first_non_void = np.argmax(board != VOID, axis=1) size = np.gcd(board.shape[0], board.shape[1])
row_last_non_void = board.shape[1] - np.argmax(board[:, ::-1] != VOID, axis=1) - 1 for row in range(0, board.shape[0], size):
col_first_non_void = np.argmax(board != VOID, axis=0)
col_last_non_void = board.shape[0] - np.argmax(board[::-1, :] != VOID, axis=0) - 1
faces = np.zeros_like(board)
size = np.gcd(board.shape[0], board.shape[1])
for row in range(0, board.shape[0], size):
for col in range(row_first_non_void[row], row_last_non_void[row], size): for col in range(row_first_non_void[row], row_last_non_void[row], size):
faces[row : row + size, col : col + size] = faces.max() + 1 faces[row : row + size, col : col + size] = faces.max() + 1
SIZE = np.gcd(*board.shape) SIZE = np.gcd(*board.shape)
# TODO: deduce this from the actual cube... # TODO: deduce this from the actual cube...
faces_wrap: dict[int, dict[str, Callable[[int, int], tuple[int, int, str]]]] faces_wrap: dict[int, dict[str, Callable[[int, int], tuple[int, int, str]]]]
if board.shape == (12, 16): # example if board.shape == (12, 16): # example
faces_wrap = { faces_wrap = {
1: { 1: {
"W": lambda y, x: (4, 4 + y, "S"), # 3N "W": lambda y, x: (4, 4 + y, "S"), # 3N
@ -73,7 +79,7 @@ if board.shape == (12, 16): # example
}, },
} }
else: else:
faces_wrap = { faces_wrap = {
1: { 1: {
"W": lambda y, x: (3 * SIZE - y - 1, 0, "E"), # 4W "W": lambda y, x: (3 * SIZE - y - 1, 0, "E"), # 4W
@ -103,8 +109,7 @@ else:
}, },
} }
def wrap_part_1(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
def wrap_part_1(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
if r0 == "E": if r0 == "E":
return y0, row_first_non_void[y0], r0 return y0, row_first_non_void[y0], r0
elif r0 == "S": elif r0 == "S":
@ -116,14 +121,14 @@ def wrap_part_1(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
assert False assert False
def wrap_part_2(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
def wrap_part_2(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
cube = faces[y0, x0] cube = faces[y0, x0]
assert r0 in faces_wrap[cube] assert r0 in faces_wrap[cube]
return faces_wrap[cube][r0](y0, x0) return faces_wrap[cube][r0](y0, x0)
def run(
def run(wrap: Callable[[int, int, str], tuple[int, int, str]]) -> tuple[int, int, str]: wrap: Callable[[int, int, str], tuple[int, int, str]],
) -> tuple[int, int, str]:
y0 = 0 y0 = 0
x0 = np.where(board[0] == EMPTY)[0][0] x0 = np.where(board[0] == EMPTY)[0][0]
r0 = "E" r0 = "E"
@ -132,7 +137,9 @@ def run(wrap: Callable[[int, int, str], tuple[int, int, str]]) -> tuple[int, int
if isinstance(direction, int): if isinstance(direction, int):
while direction > 0: while direction > 0:
if r0 == "E": if r0 == "E":
xi = np.where(board[y0, x0 + 1 : x0 + direction + 1] == WALL)[0] xi = np.where(
board[y0, x0 + 1 : x0 + direction + 1] == WALL
)[0]
if len(xi): if len(xi):
x0 = x0 + xi[0] x0 = x0 + xi[0]
direction = 0 direction = 0
@ -148,10 +155,14 @@ def run(wrap: Callable[[int, int, str], tuple[int, int, str]]) -> tuple[int, int
x0 = row_last_non_void[y0] x0 = row_last_non_void[y0]
direction = 0 direction = 0
else: else:
direction = direction - (row_last_non_void[y0] - x0) - 1 direction = (
direction - (row_last_non_void[y0] - x0) - 1
)
y0, x0, r0 = y0_t, x0_t, r0_t y0, x0, r0 = y0_t, x0_t, r0_t
elif r0 == "S": elif r0 == "S":
yi = np.where(board[y0 + 1 : y0 + direction + 1, x0] == WALL)[0] yi = np.where(
board[y0 + 1 : y0 + direction + 1, x0] == WALL
)[0]
if len(yi): if len(yi):
y0 = y0 + yi[0] y0 = y0 + yi[0]
direction = 0 direction = 0
@ -167,7 +178,9 @@ def run(wrap: Callable[[int, int, str], tuple[int, int, str]]) -> tuple[int, int
y0 = col_last_non_void[x0] y0 = col_last_non_void[x0]
direction = 0 direction = 0
else: else:
direction = direction - (col_last_non_void[x0] - y0) - 1 direction = (
direction - (col_last_non_void[x0] - y0) - 1
)
y0, x0, r0 = y0_t, x0_t, r0_t y0, x0, r0 = y0_t, x0_t, r0_t
elif r0 == "W": elif r0 == "W":
left = max(x0 - direction - 1, 0) left = max(x0 - direction - 1, 0)
@ -175,7 +188,10 @@ def run(wrap: Callable[[int, int, str], tuple[int, int, str]]) -> tuple[int, int
if len(xi): if len(xi):
x0 = left + xi[-1] + 1 x0 = left + xi[-1] + 1
direction = 0 direction = 0
elif x0 - direction >= 0 and board[y0, x0 - direction] == EMPTY: elif (
x0 - direction >= 0
and board[y0, x0 - direction] == EMPTY
):
x0 = x0 - direction x0 = x0 - direction
direction = 0 direction = 0
else: else:
@ -184,7 +200,9 @@ def run(wrap: Callable[[int, int, str], tuple[int, int, str]]) -> tuple[int, int
x0 = row_first_non_void[y0] x0 = row_first_non_void[y0]
direction = 0 direction = 0
else: else:
direction = direction - (x0 - row_first_non_void[y0]) - 1 direction = (
direction - (x0 - row_first_non_void[y0]) - 1
)
y0, x0, r0 = y0_t, x0_t, r0_t y0, x0, r0 = y0_t, x0_t, r0_t
elif r0 == "N": elif r0 == "N":
top = max(y0 - direction - 1, 0) top = max(y0 - direction - 1, 0)
@ -192,7 +210,10 @@ def run(wrap: Callable[[int, int, str], tuple[int, int, str]]) -> tuple[int, int
if len(yi): if len(yi):
y0 = top + yi[-1] + 1 y0 = top + yi[-1] + 1
direction = 0 direction = 0
elif y0 - direction >= 0 and board[y0 - direction, x0] == EMPTY: elif (
y0 - direction >= 0
and board[y0 - direction, x0] == EMPTY
):
y0 = y0 - direction y0 = y0 - direction
direction = 0 direction = 0
else: else:
@ -201,7 +222,9 @@ def run(wrap: Callable[[int, int, str], tuple[int, int, str]]) -> tuple[int, int
y0 = col_first_non_void[x0] y0 = col_first_non_void[x0]
direction = 0 direction = 0
else: else:
direction = direction - (y0 - col_first_non_void[x0]) - 1 direction = (
direction - (y0 - col_first_non_void[x0]) - 1
)
y0, x0, r0 = y0_t, x0_t, r0_t y0, x0, r0 = y0_t, x0_t, r0_t
else: else:
r0 = { r0 = {
@ -213,11 +236,8 @@ def run(wrap: Callable[[int, int, str], tuple[int, int, str]]) -> tuple[int, int
return y0, x0, r0 return y0, x0, r0
y1, x1, r1 = run(wrap_part_1)
yield 1000 * (1 + y1) + 4 * (1 + x1) + SCORES[r1]
y1, x1, r1 = run(wrap_part_1) y2, x2, r2 = run(wrap_part_2)
answer_1 = 1000 * (1 + y1) + 4 * (1 + x1) + SCORES[r1] yield 1000 * (1 + y2) + 4 * (1 + x2) + SCORES[r2]
print(f"answer 1 is {answer_1}")
y2, x2, r2 = run(wrap_part_2)
answer_2 = 1000 * (1 + y2) + 4 * (1 + x2) + SCORES[r2]
print(f"answer 2 is {answer_2}")

View File

@ -1,6 +1,8 @@
import itertools import itertools
import sys
from collections import defaultdict from collections import defaultdict
from typing import Any, Iterator
from ..base import BaseSolver
Directions = list[ Directions = list[
tuple[ tuple[
@ -18,7 +20,7 @@ DIRECTIONS: Directions = [
def min_max_yx(positions: set[tuple[int, int]]) -> tuple[int, int, int, int]: def min_max_yx(positions: set[tuple[int, int]]) -> tuple[int, int, int, int]:
ys, xs = {y for y, x in positions}, {x for y, x in positions} ys, xs = {y for y, _x in positions}, {x for _y, x in positions}
return min(ys), min(xs), max(ys), max(xs) return min(ys), min(xs), max(ys), max(xs)
@ -69,30 +71,33 @@ def round(
directions.append(directions.pop(0)) directions.append(directions.pop(0))
POSITIONS = { class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
POSITIONS = {
(i, j) (i, j)
for i, row in enumerate(sys.stdin.read().splitlines()) for i, row in enumerate(input.splitlines())
for j, col in enumerate(row) for j, col in enumerate(row)
if col == "#" if col == "#"
} }
# === part 1 === # === part 1 ===
p1, d1 = POSITIONS.copy(), DIRECTIONS.copy() p1, d1 = POSITIONS.copy(), DIRECTIONS.copy()
for r in range(10): for _ in range(10):
round(p1, d1) round(p1, d1)
min_y, min_x, max_y, max_x = min_max_yx(p1) min_y, min_x, max_y, max_x = min_max_yx(p1)
answer_1 = sum( yield sum(
(y, x) not in p1 for y in range(min_y, max_y + 1) for x in range(min_x, max_x + 1) (y, x) not in p1
) for y in range(min_y, max_y + 1)
print(f"answer 1 is {answer_1}") for x in range(min_x, max_x + 1)
)
# === part 2 === # === part 2 ===
p2, d2 = POSITIONS.copy(), DIRECTIONS.copy() p2, d2 = POSITIONS.copy(), DIRECTIONS.copy()
answer_2 = 0 answer_2 = 0
while True: while True:
answer_2 += 1 answer_2 += 1
backup = p2.copy() backup = p2.copy()
round(p2, d2) round(p2, d2)
@ -100,4 +105,4 @@ while True:
if backup == p2: if backup == p2:
break break
print(f"answer 2 is {answer_2}") yield answer_2

View File

@ -1,36 +1,46 @@
import heapq import heapq
import math import math
import sys
from collections import defaultdict from collections import defaultdict
from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
winds = {
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
winds = {
(i - 1, j - 1, lines[i][j]) (i - 1, j - 1, lines[i][j])
for i in range(1, len(lines) - 1) for i in range(1, len(lines) - 1)
for j in range(1, len(lines[i]) - 1) for j in range(1, len(lines[i]) - 1)
if lines[i][j] != "." if lines[i][j] != "."
} }
n_rows, n_cols = len(lines) - 2, len(lines[0]) - 2 n_rows, n_cols = len(lines) - 2, len(lines[0]) - 2
CYCLE = math.lcm(n_rows, n_cols) CYCLE = math.lcm(n_rows, n_cols)
east_winds = [{j for j in range(n_cols) if (i, j, ">") in winds} for i in range(n_rows)] east_winds = [
west_winds = [{j for j in range(n_cols) if (i, j, "<") in winds} for i in range(n_rows)] {j for j in range(n_cols) if (i, j, ">") in winds} for i in range(n_rows)
north_winds = [ ]
west_winds = [
{j for j in range(n_cols) if (i, j, "<") in winds} for i in range(n_rows)
]
north_winds = [
{i for i in range(n_rows) if (i, j, "^") in winds} for j in range(n_cols) {i for i in range(n_rows) if (i, j, "^") in winds} for j in range(n_cols)
] ]
south_winds = [ south_winds = [
{i for i in range(n_rows) if (i, j, "v") in winds} for j in range(n_cols) {i for i in range(n_rows) if (i, j, "v") in winds} for j in range(n_cols)
] ]
def run(start: tuple[int, int], start_cycle: int, end: tuple[int, int]):
def run(start: tuple[int, int], start_cycle: int, end: tuple[int, int]):
def heuristic(y: int, x: int) -> int: def heuristic(y: int, x: int) -> int:
return abs(end[0] - y) + abs(end[1] - x) return abs(end[0] - y) + abs(end[1] - x)
# (distance + heuristic, distance, (start_pos, cycle)) # (distance + heuristic, distance, (start_pos, cycle))
queue = [(heuristic(start[0], start[1]), 0, ((start[0], start[1]), start_cycle))] queue = [
(heuristic(start[0], start[1]), 0, ((start[0], start[1]), start_cycle))
]
visited: set[tuple[tuple[int, int], int]] = set() visited: set[tuple[tuple[int, int], int]] = set()
distances: dict[tuple[int, int], dict[int, int]] = defaultdict(lambda: {}) distances: dict[tuple[int, int], dict[int, int]] = defaultdict(lambda: {})
@ -54,13 +64,17 @@ def run(start: tuple[int, int], start_cycle: int, end: tuple[int, int]):
n_cycle = (cycle + 1) % CYCLE n_cycle = (cycle + 1) % CYCLE
if (ty, tx) == end: if (ty, tx) == end:
heapq.heappush(queue, (distance + 1, distance + 1, ((ty, tx), n_cycle))) heapq.heappush(
queue, (distance + 1, distance + 1, ((ty, tx), n_cycle))
)
break break
if ((ty, tx), n_cycle) in visited: if ((ty, tx), n_cycle) in visited:
continue continue
if (ty, tx) != start and (ty < 0 or tx < 0 or ty >= n_rows or tx >= n_cols): if (ty, tx) != start and (
ty < 0 or tx < 0 or ty >= n_rows or tx >= n_cols
):
continue continue
if (ty, tx) != start: if (ty, tx) != start:
@ -75,24 +89,29 @@ def run(start: tuple[int, int], start_cycle: int, end: tuple[int, int]):
heapq.heappush( heapq.heappush(
queue, queue,
((heuristic(ty, tx) + distance + 1, distance + 1, ((ty, tx), n_cycle))), (
(
heuristic(ty, tx) + distance + 1,
distance + 1,
((ty, tx), n_cycle),
)
),
) )
return distances, next(iter(distances[end].values())) return distances, next(iter(distances[end].values()))
start = (
start = (
-1, -1,
next(j for j in range(1, len(lines[0]) - 1) if lines[0][j] == ".") - 1, next(j for j in range(1, len(lines[0]) - 1) if lines[0][j] == ".") - 1,
) )
end = ( end = (
n_rows, n_rows,
next(j for j in range(1, len(lines[-1]) - 1) if lines[-1][j] == ".") - 1, next(j for j in range(1, len(lines[-1]) - 1) if lines[-1][j] == ".") - 1,
) )
distances_1, forward_1 = run(start, 0, end) distances_1, forward_1 = run(start, 0, end)
print(f"answer 1 is {forward_1}") yield forward_1
distances_2, return_1 = run(end, next(iter(distances_1[end].keys())), start) distances_2, return_1 = run(end, next(iter(distances_1[end].keys())), start)
distances_3, forward_2 = run(start, next(iter(distances_2[start].keys())), end) _distances_3, forward_2 = run(start, next(iter(distances_2[start].keys())), end)
print(f"answer 2 is {forward_1 + return_1 + forward_2}") yield forward_1 + return_1 + forward_2

View File

@ -1,19 +1,22 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
coeffs = {"2": 2, "1": 1, "0": 0, "-": -1, "=": -2}
def snafu2number(number: str) -> int: class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
coeffs = {"2": 2, "1": 1, "0": 0, "-": -1, "=": -2}
def snafu2number(number: str) -> int:
value = 0 value = 0
for c in number: for c in number:
value *= 5 value *= 5
value += coeffs[c] value += coeffs[c]
return value return value
def number2snafu(number: int) -> str:
def number2snafu(number: int) -> str:
values = ["0", "1", "2", "=", "-"] values = ["0", "1", "2", "=", "-"]
res = "" res = ""
while number > 0: while number > 0:
@ -22,6 +25,4 @@ def number2snafu(number: int) -> str:
number = number // 5 + int(mod >= 3) number = number // 5 + int(mod >= 3)
return "".join(reversed(res)) return "".join(reversed(res))
yield number2snafu(sum(map(snafu2number, lines)))
answer_1 = number2snafu(sum(map(snafu2number, lines)))
print(f"answer 1 is {answer_1}")

View File

@ -1,23 +1,28 @@
import string import string
import sys from typing import Any, Iterator
lines = [line.strip() for line in sys.stdin.readlines()] from ..base import BaseSolver
# extract content of each part
parts = [(set(line[: len(line) // 2]), set(line[len(line) // 2 :])) for line in lines]
# priorities class Solver(BaseSolver):
priorities = {c: i + 1 for i, c in enumerate(string.ascii_letters)} def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
# part 1 # extract content of each part
part1 = sum(priorities[c] for p1, p2 in parts for c in p1.intersection(p2)) parts = [
print(f"answer 1 is {part1}") (set(line[: len(line) // 2]), set(line[len(line) // 2 :])) for line in lines
]
# part 2 # priorities
n_per_group = 3 priorities = {c: i + 1 for i, c in enumerate(string.ascii_letters)}
part2 = sum(
# part 1
yield sum(priorities[c] for p1, p2 in parts for c in p1.intersection(p2))
# part 2
n_per_group = 3
yield sum(
priorities[c] priorities[c]
for i in range(0, len(lines), n_per_group) for i in range(0, len(lines), n_per_group)
for c in set(lines[i]).intersection(*lines[i + 1 : i + n_per_group]) for c in set(lines[i]).intersection(*lines[i + 1 : i + n_per_group])
) )
print(f"answer 2 is {part2}")

View File

@ -1,6 +1,6 @@
import sys from typing import Any, Iterator
lines = [line.strip() for line in sys.stdin.readlines()] from ..base import BaseSolver
def make_range(value: str) -> set[int]: def make_range(value: str) -> set[int]:
@ -8,10 +8,13 @@ def make_range(value: str) -> set[int]:
return set(range(int(parts[0]), int(parts[1]) + 1)) return set(range(int(parts[0]), int(parts[1]) + 1))
sections = [tuple(make_range(part) for part in line.split(",")) for line in lines] class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
answer_1 = sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections) sections = [
print(f"answer 1 is {answer_1}") tuple(make_range(part) for part in line.split(",")) for line in lines
]
answer_2 = sum(bool(s1.intersection(s2)) for s1, s2 in sections) yield sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections)
print(f"answer 1 is {answer_2}") yield sum(bool(s1.intersection(s2)) for s1, s2 in sections)

View File

@ -1,41 +1,43 @@
import copy import copy
import sys from typing import Any, Iterator
blocks_s, moves_s = (part.splitlines() for part in sys.stdin.read().split("\n\n")) from ..base import BaseSolver
blocks: dict[str, list[str]] = {stack: [] for stack in blocks_s[-1].split()}
# this codes assumes that the lines are regular, i.e., 4 characters per "crate" in the class Solver(BaseSolver):
# form of '[X] ' (including the trailing space) def solve(self, input: str) -> Iterator[Any]:
# blocks_s, moves_s = (part.splitlines() for part in input.split("\n\n"))
for block in blocks_s[-2::-1]:
blocks: dict[str, list[str]] = {stack: [] for stack in blocks_s[-1].split()}
# this codes assumes that the lines are regular, i.e., 4 characters per "crate" in the
# form of '[X] ' (including the trailing space)
#
for block in blocks_s[-2::-1]:
for stack, index in zip(blocks, range(0, len(block), 4)): for stack, index in zip(blocks, range(0, len(block), 4)):
crate = block[index + 1 : index + 2].strip() crate = block[index + 1 : index + 2].strip()
if crate: if crate:
blocks[stack].append(crate) blocks[stack].append(crate)
# part 1 - deep copy for part 2 # part 1 - deep copy for part 2
blocks_1 = copy.deepcopy(blocks) blocks_1 = copy.deepcopy(blocks)
for move in moves_s: for move in moves_s:
_, count_s, _, from_, _, to_ = move.strip().split() _, count_s, _, from_, _, to_ = move.strip().split()
for _i in range(int(count_s)): for _i in range(int(count_s)):
blocks_1[to_].append(blocks_1[from_].pop()) blocks_1[to_].append(blocks_1[from_].pop())
# part 2 # part 2
blocks_2 = copy.deepcopy(blocks) blocks_2 = copy.deepcopy(blocks)
for move in moves_s: for move in moves_s:
_, count_s, _, from_, _, to_ = move.strip().split() _, count_s, _, from_, _, to_ = move.strip().split()
count = int(count_s) count = int(count_s)
blocks_2[to_].extend(blocks_2[from_][-count:]) blocks_2[to_].extend(blocks_2[from_][-count:])
del blocks_2[from_][-count:] del blocks_2[from_][-count:]
answer_1 = "".join(s[-1] for s in blocks_1.values()) yield "".join(s[-1] for s in blocks_1.values())
print(f"answer 1 is {answer_1}") yield "".join(s[-1] for s in blocks_2.values())
answer_2 = "".join(s[-1] for s in blocks_2.values())
print(f"answer 2 is {answer_2}")

View File

@ -1,4 +1,6 @@
import sys from typing import Any, Iterator
from ..base import BaseSolver
def index_of_first_n_differents(data: str, n: int) -> int: def index_of_first_n_differents(data: str, n: int) -> int:
@ -8,8 +10,7 @@ def index_of_first_n_differents(data: str, n: int) -> int:
return -1 return -1
data = sys.stdin.read().strip() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
yield index_of_first_n_differents(input, 4)
print(f"answer 1 is {index_of_first_n_differents(data, 4)}") yield index_of_first_n_differents(input, 14)
print(f"answer 2 is {index_of_first_n_differents(data, 14)}")

View File

@ -1,30 +1,35 @@
import sys
from pathlib import Path from pathlib import Path
from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# we are going to use Path to create path and go up/down in the file tree since it
# implements everything we need
#
# we can use .resolve() to get normalized path, although this will add C:\ to all paths
# on Windows but that is not an issue since only the sizes matter
#
# mapping from path to list of files or directories class Solver(BaseSolver):
trees: dict[Path, list[Path]] = {} def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
# mapping from paths to either size (for file) or -1 for directory # we are going to use Path to create path and go up/down in the file tree since it
sizes: dict[Path, int] = {} # implements everything we need
#
# we can use .resolve() to get normalized path, although this will add C:\ to all paths
# on Windows but that is not an issue since only the sizes matter
#
# first line must be a cd otherwise we have no idea where we are # mapping from path to list of files or directories
assert lines[0].startswith("$ cd") trees: dict[Path, list[Path]] = {}
base_path = Path(lines[0].strip("$").split()[1]).resolve()
cur_path = base_path
trees[cur_path] = [] # mapping from paths to either size (for file) or -1 for directory
sizes[cur_path] = -1 sizes: dict[Path, int] = {}
for line in lines[1:]: # first line must be a cd otherwise we have no idea where we are
assert lines[0].startswith("$ cd")
base_path = Path(lines[0].strip("$").split()[1]).resolve()
cur_path = base_path
trees[cur_path] = []
sizes[cur_path] = -1
for line in lines[1:]:
# command # command
if line.startswith("$"): if line.startswith("$"):
parts = line.strip("$").strip().split() parts = line.strip("$").strip().split()
@ -53,8 +58,7 @@ for line in lines[1:]:
trees[cur_path].append(path) trees[cur_path].append(path)
sizes[path] = size sizes[path] = size
def compute_size(path: Path) -> int:
def compute_size(path: Path) -> int:
size = sizes[path] size = sizes[path]
if size >= 0: if size >= 0:
@ -62,19 +66,16 @@ def compute_size(path: Path) -> int:
return sum(compute_size(sub) for sub in trees[path]) return sum(compute_size(sub) for sub in trees[path])
acc_sizes = {path: compute_size(path) for path in trees}
acc_sizes = {path: compute_size(path) for path in trees} # part 1
yield sum(size for size in acc_sizes.values() if size <= 100_000)
# part 1 # part 2
answer_1 = sum(size for size in acc_sizes.values() if size <= 100_000) total_space = 70_000_000
print(f"answer 1 is {answer_1}") update_space = 30_000_000
free_space = total_space - acc_sizes[base_path]
# part 2 to_free_space = update_space - free_space
total_space = 70_000_000
update_space = 30_000_000
free_space = total_space - acc_sizes[base_path]
to_free_space = update_space - free_space yield min(size for size in acc_sizes.values() if size >= to_free_space)
answer_2 = min(size for size in acc_sizes.values() if size >= to_free_space)
print(f"answer 2 is {answer_2}")

View File

@ -1,15 +1,20 @@
import sys from typing import Any, Iterator
import numpy as np import numpy as np
from numpy.typing import NDArray from numpy.typing import NDArray
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
trees = np.array([[int(x) for x in row] for row in lines])
# answer 1 class Solver(BaseSolver):
highest_trees = np.ones(trees.shape + (4,), dtype=int) * -1 def solve(self, input: str) -> Iterator[Any]:
highest_trees[1:-1, 1:-1] = [ lines = [line.strip() for line in input.splitlines()]
trees = np.array([[int(x) for x in row] for row in lines])
# answer 1
highest_trees = np.ones(trees.shape + (4,), dtype=int) * -1
highest_trees[1:-1, 1:-1] = [
[ [
[ [
trees[:i, j].max(), trees[:i, j].max(),
@ -20,13 +25,11 @@ highest_trees[1:-1, 1:-1] = [
for j in range(1, trees.shape[1] - 1) for j in range(1, trees.shape[1] - 1)
] ]
for i in range(1, trees.shape[0] - 1) for i in range(1, trees.shape[0] - 1)
] ]
answer_1 = (highest_trees.min(axis=2) < trees).sum() yield (highest_trees.min(axis=2) < trees).sum()
print(f"answer 1 is {answer_1}")
def viewing_distance(row_of_trees: NDArray[np.int_], value: int) -> int:
def viewing_distance(row_of_trees: NDArray[np.int_], value: int) -> int:
w = np.where(row_of_trees >= value)[0] w = np.where(row_of_trees >= value)[0]
if not w.size: if not w.size:
@ -34,10 +37,9 @@ def viewing_distance(row_of_trees: NDArray[np.int_], value: int) -> int:
return w[0] + 1 return w[0] + 1
# answer 2
# answer 2 v_distances = np.zeros(trees.shape + (4,), dtype=int)
v_distances = np.zeros(trees.shape + (4,), dtype=int) v_distances[1:-1, 1:-1, :] = [
v_distances[1:-1, 1:-1, :] = [
[ [
[ [
viewing_distance(trees[i - 1 :: -1, j], trees[i, j]), viewing_distance(trees[i - 1 :: -1, j], trees[i, j]),
@ -48,6 +50,5 @@ v_distances[1:-1, 1:-1, :] = [
for j in range(1, trees.shape[1] - 1) for j in range(1, trees.shape[1] - 1)
] ]
for i in range(1, trees.shape[0] - 1) for i in range(1, trees.shape[0] - 1)
] ]
answer_2 = np.prod(v_distances, axis=2).max() yield np.prod(v_distances, axis=2).max()
print(f"answer 2 is {answer_2}")

View File

@ -1,7 +1,10 @@
import sys import itertools as it
from typing import Any, Iterator
import numpy as np import numpy as np
from ..base import BaseSolver
def move(head: tuple[int, int], command: str) -> tuple[int, int]: def move(head: tuple[int, int], command: str) -> tuple[int, int]:
h_col, h_row = head h_col, h_row = head
@ -43,17 +46,14 @@ def run(commands: list[str], n_blocks: int) -> list[tuple[int, int]]:
return visited return visited
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
# flatten the commands # flatten the commands
commands: list[str] = [] commands = list(
for line in lines: it.chain(*(p[0] * int(p[1]) for line in lines if (p := line.split())))
d, c = line.split() )
commands.extend(d * int(c))
yield len(set(run(commands, n_blocks=2)))
visited_1 = run(commands, n_blocks=2) yield len(set(run(commands, n_blocks=10)))
print(f"answer 1 is {len(set(visited_1))}")
visited_2 = run(commands, n_blocks=10)
print(f"answer 2 is {len(set(visited_2))}")

View File

@ -1,27 +1,9 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
lookups_1 = {str(d): d for d in range(1, 10)}
lookups_2 = lookups_1 | {
d: i + 1
for i, d in enumerate(
(
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
)
)
}
def find_values(lookups: dict[str, int]) -> list[int]: def find_values(lines: list[str], lookups: dict[str, int]) -> list[int]:
values: list[int] = [] values: list[int] = []
for line in filter(bool, lines): for line in filter(bool, lines):
@ -41,5 +23,27 @@ def find_values(lookups: dict[str, int]) -> list[int]:
return values return values
print(f"answer 1 is {sum(find_values(lookups_1))}") class Solver(BaseSolver):
print(f"answer 2 is {sum(find_values(lookups_2))}") def solve(self, input: str) -> Iterator[Any]:
lookups_1 = {str(d): d for d in range(1, 10)}
lookups_2 = lookups_1 | {
d: i + 1
for i, d in enumerate(
(
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
)
)
}
lines = input.splitlines()
yield sum(find_values(lines, lookups_1))
yield sum(find_values(lines, lookups_2))

View File

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

View File

@ -1,18 +1,22 @@
import sys from typing import Any, Iterator
import numpy as np import numpy as np
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
data = np.array([[c == "#" for c in line] for line in lines])
rows = {c for c in range(data.shape[0]) if not data[c, :].any()}
columns = {c for c in range(data.shape[1]) if not data[:, c].any()}
galaxies_y, galaxies_x = np.where(data) # type: ignore
def compute_total_distance(expansion: int) -> int: class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
data = np.array([[c == "#" for c in line] for line in lines])
rows = {c for c in range(data.shape[0]) if not data[c, :].any()}
columns = {c for c in range(data.shape[1]) if not data[:, c].any()}
galaxies_y, galaxies_x = np.where(data) # type: ignore
def compute_total_distance(expansion: int) -> int:
distances: list[int] = [] distances: list[int] = []
for g1 in range(len(galaxies_y)): for g1 in range(len(galaxies_y)):
x1, y1 = int(galaxies_x[g1]), int(galaxies_y[g1]) x1, y1 = int(galaxies_x[g1]), int(galaxies_y[g1])
@ -31,11 +35,8 @@ def compute_total_distance(expansion: int) -> int:
distances.append(dx + dy) distances.append(dx + dy)
return sum(distances) return sum(distances)
# part 1
yield compute_total_distance(2)
# part 1 # part 2
answer_1 = compute_total_distance(2) yield compute_total_distance(1000000)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = compute_total_distance(1000000)
print(f"answer 2 is {answer_2}")

View File

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

View File

@ -1,5 +1,6 @@
import sys from typing import Any, Callable, Iterator, Literal
from typing import Callable, Literal
from ..base import BaseSolver
def split(block: list[str], axis: Literal[0, 1], count: int) -> int: def split(block: list[str], axis: Literal[0, 1], count: int) -> int:
@ -25,19 +26,18 @@ def split(block: list[str], axis: Literal[0, 1], count: int) -> int:
return 0 return 0
blocks = [block.splitlines() for block in sys.stdin.read().split("\n\n")] class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
blocks = [block.splitlines() for block in input.split("\n\n")]
# part 1
# part 1 yield sum(
answer_1 = sum(
split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0) split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0)
for block in blocks for block in blocks
) )
print(f"answer 1 is {answer_1}")
# part 2 # part 2
answer_2 = sum( yield sum(
split(block, axis=1, count=1) + 100 * split(block, axis=0, count=1) split(block, axis=1, count=1) + 100 * split(block, axis=0, count=1)
for block in blocks for block in blocks
) )
print(f"answer 2 is {answer_2}")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,18 @@
import math import math
import sys from typing import Any, Iterator, Literal, TypeAlias, cast
from typing import Literal, TypeAlias, cast
from ..base import BaseSolver
CubeType: TypeAlias = Literal["red", "blue", "green"] CubeType: TypeAlias = Literal["red", "blue", "green"]
MAX_CUBES: dict[CubeType, int] = {"red": 12, "green": 13, "blue": 14} MAX_CUBES: dict[CubeType, int] = {"red": 12, "green": 13, "blue": 14}
# parse games
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
games: dict[int, list[dict[CubeType, int]]] = {} def solve(self, input: str) -> Iterator[Any]:
for line in filter(bool, lines): lines = input.splitlines()
games: dict[int, list[dict[CubeType, int]]] = {}
for line in filter(bool, lines):
id_part, sets_part = line.split(":") id_part, sets_part = line.split(":")
games[int(id_part.split(" ")[-1])] = [ games[int(id_part.split(" ")[-1])] = [
@ -21,8 +24,7 @@ for line in filter(bool, lines):
for cube_set_s in sets_part.strip().split(";") for cube_set_s in sets_part.strip().split(";")
] ]
# part 1 yield sum(
answer_1 = sum(
id id
for id, set_of_cubes in games.items() for id, set_of_cubes in games.items()
if all( if all(
@ -30,14 +32,12 @@ answer_1 = sum(
for cube_set in set_of_cubes for cube_set in set_of_cubes
for cube, n_cubes in cube_set.items() for cube, n_cubes in cube_set.items()
) )
) )
print(f"answer 1 is {answer_1}")
# part 2 yield sum(
answer_2 = sum(
math.prod( math.prod(
max(cube_set.get(cube, 0) for cube_set in set_of_cubes) for cube in MAX_CUBES max(cube_set.get(cube, 0) for cube_set in set_of_cubes)
for cube in MAX_CUBES
) )
for set_of_cubes in games.values() for set_of_cubes in games.values()
) )
print(f"answer 2 is {answer_2}")

View File

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

View File

@ -1,9 +1,6 @@
import logging from typing import Any, Iterator
import os
import sys
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
def reachable( def reachable(
@ -21,35 +18,39 @@ def reachable(
return tiles return tiles
map = sys.stdin.read().splitlines() class Solver(BaseSolver):
start = next( def solve(self, input: str) -> Iterator[Any]:
(i, j) for i in range(len(map)) for j in range(len(map[i])) if map[i][j] == "S" map = input.splitlines()
) start = next(
(i, j)
for i in range(len(map))
for j in range(len(map[i]))
if map[i][j] == "S"
)
# part 1 # part 1
answer_1 = len(reachable(map, {start}, 6 if len(map) < 20 else 64)) yield len(reachable(map, {start}, 6 if len(map) < 20 else 64))
print(f"answer 1 is {answer_1}")
# part 2 # part 2
# the initial map is a square and contains an empty rhombus whose diameter is the size # the initial map is a square and contains an empty rhombus whose diameter is
# of the map, and has only empty cells around the middle row and column # the size of the map, and has only empty cells around the middle row and column
# #
# after ~n/2 steps, the first map is filled with a rhombus, after that we get a bigger # after ~n/2 steps, the first map is filled with a rhombus, after that we get a
# rhombus every n steps # bigger rhombus every n steps
# #
# we are going to find the number of cells reached for the initial rhombus, n steps # we are going to find the number of cells reached for the initial rhombus, n
# after and n * 2 steps after # steps after and n * 2 steps after
# #
cycle = len(map) cycle = len(map)
rhombus = (len(map) - 3) // 2 + 1 rhombus = (len(map) - 3) // 2 + 1
values: list[int] = [] values: list[int] = []
values.append(len(tiles := reachable(map, {start}, rhombus))) values.append(len(tiles := reachable(map, {start}, rhombus)))
values.append(len(tiles := reachable(map, tiles, cycle))) values.append(len(tiles := reachable(map, tiles, cycle)))
values.append(len(tiles := reachable(map, tiles, cycle))) values.append(len(tiles := reachable(map, tiles, cycle)))
if logging.root.getEffectiveLevel() == logging.INFO: if self.verbose:
n_rows, n_cols = len(map), len(map[0]) n_rows, n_cols = len(map), len(map[0])
rows = [ rows = [
@ -65,42 +66,42 @@ if logging.root.getEffectiveLevel() == logging.INFO:
if (i // cycle) % 2 == (j // cycle) % 2: if (i // cycle) % 2 == (j // cycle) % 2:
rows[i][j] = f"\033[91m{rows[i][j]}\033[0m" rows[i][j] = f"\033[91m{rows[i][j]}\033[0m"
print("\n".join("".join(row) for row in rows)) for row in rows:
self.logger.info("".join(row))
self.logger.info(f"values to fit: {values}")
logging.info(f"values to fit: {values}") # version 1:
#
# after 3 cycles, the figure looks like the following:
#
# I M D
# I J A K D
# H A F A L
# C E A K B
# C G B
#
# after 4 cycles, the figure looks like the following:
#
# I M D
# I J A K D
# I J A B A K D
# H A B A B A L
# C E A B A N F
# C E A N F
# C G F
#
# the 'radius' of the rhombus is the number of cycles minus 1
#
# the 4 'corner' (M, H, L, G) are counted once, the blocks with a corner triangle (D, I,
# C, B) are each counted radius times, the blocks with everything but one corner (J, K,
# E, N) are each counted radius - 1 times
#
# there are two versions of the whole block, A and B in the above (or odd and even),
# depending on the number of cycles, either A or B will be in the center
#
# version 1: counts = [
#
# after 3 cycles, the figure looks like the following:
#
# I M D
# I J A K D
# H A F A L
# C E A K B
# C G B
#
# after 4 cycles, the figure looks like the following:
#
# I M D
# I J A K D
# I J A B A K D
# H A B A B A L
# C E A B A N F
# C E A N F
# C G F
#
# the 'radius' of the rhombus is the number of cycles minus 1
#
# the 4 'corner' (M, H, L, G) are counted once, the blocks with a corner triangle (D, I,
# C, B) are each counted radius times, the blocks with everything but one corner (J, K,
# E, N) are each counted radius - 1 times
#
# there are two versions of the whole block, A and B in the above (or odd and even),
# depending on the number of cycles, either A or B will be in the center
#
counts = [
[ [
sum( sum(
(i, j) in tiles (i, j) in tiles
@ -110,40 +111,40 @@ counts = [
for cj in range(-2, 3) for cj in range(-2, 3)
] ]
for ci in range(-2, 3) for ci in range(-2, 3)
] ]
radius = (26501365 - rhombus) // cycle - 1 radius = (26501365 - rhombus) // cycle - 1
A = counts[2][2] if radius % 2 == 0 else counts[2][1] A = counts[2][2] if radius % 2 == 0 else counts[2][1]
B = counts[2][2] if radius % 2 == 1 else counts[2][1] B = counts[2][2] if radius % 2 == 1 else counts[2][1]
answer_2 = ( answer_2 = (
(radius + 1) * A (radius + 1) * A
+ radius * B + radius * B
+ 2 * radius * (radius + 1) // 2 * A + 2 * radius * (radius + 1) // 2 * A
+ 2 * radius * (radius - 1) // 2 * B + 2 * radius * (radius - 1) // 2 * B
+ sum(counts[i][j] for i, j in ((0, 2), (-1, 2), (2, 0), (2, -1))) + sum(counts[i][j] for i, j in ((0, 2), (-1, 2), (2, 0), (2, -1)))
+ sum(counts[i][j] for i, j in ((0, 1), (0, 3), (-1, 1), (-1, 3))) * (radius + 1) + sum(counts[i][j] for i, j in ((0, 1), (0, 3), (-1, 1), (-1, 3)))
* (radius + 1)
+ sum(counts[i][j] for i, j in ((1, 1), (1, 3), (-2, 1), (-2, 3))) * radius + sum(counts[i][j] for i, j in ((1, 1), (1, 3), (-2, 1), (-2, 3))) * radius
) )
print(f"answer 2 (v1) is {answer_2}") print(f"answer 2 (v1) is {answer_2}")
# version 2: fitting a polynomial # version 2: fitting a polynomial
# #
# the value we are interested in (26501365) can be written as R + K * C where R is the # the value we are interested in (26501365) can be written as R + K * C where R is the
# step at which we find the first rhombus, and K the repeat step, so instead of fitting # step at which we find the first rhombus, and K the repeat step, so instead of fitting
# for X values (R, R + K, R + 2 K), we are going to fit for (0, 1, 2), giving us much # for X values (R, R + K, R + 2 K), we are going to fit for (0, 1, 2), giving us much
# simpler equation for the a, b and c coefficient # simpler equation for the a, b and c coefficient
# #
# we get: # we get:
# - (a * 0² + b * 0 + c) = y1 => c = y1 # - (a * 0² + b * 0 + c) = y1 => c = y1
# - (a * 1² + b * 1 + c) = y2 => a + b = y2 - y1 # - (a * 1² + b * 1 + c) = y2 => a + b = y2 - y1
# => b = y2 - y1 - a # => b = y2 - y1 - a
# - (a * 2² + b * 2 + c) = y3 => 4a + 2b = y3 - y1 # - (a * 2² + b * 2 + c) = y3 => 4a + 2b = y3 - y1
# => 4a + 2(y2 - y1 - a) = y3 - y1 # => 4a + 2(y2 - y1 - a) = y3 - y1
# => a = (y1 + y3) / 2 - y2 # => a = (y1 + y3) / 2 - y2
# #
y1, y2, y3 = values y1, y2, y3 = values
a, b, c = (y1 + y3) // 2 - y2, 2 * y2 - (3 * y1 + y3) // 2, y1 a, b, c = (y1 + y3) // 2 - y2, 2 * y2 - (3 * y1 + y3) // 2, y1
n = (26501365 - rhombus) // cycle n = (26501365 - rhombus) // cycle
answer_2 = a * n * n + b * n + c yield a * n * n + b * n + c
print(f"answer 2 (v2) is {answer_2}")

View File

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

View File

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

View File

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

View File

@ -1,25 +1,25 @@
import sys # pyright: reportUnknownMemberType=false
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,15 +1,20 @@
import string import string
import sys
from collections import defaultdict from collections import defaultdict
from typing import Any, Iterator
from ..base import BaseSolver
NOT_A_SYMBOL = "." + string.digits NOT_A_SYMBOL = "." + string.digits
lines = sys.stdin.read().splitlines()
values: list[int] = [] class Solver(BaseSolver):
gears: dict[tuple[int, int], list[int]] = defaultdict(list) def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
for i, line in enumerate(lines): values: list[int] = []
gears: dict[tuple[int, int], list[int]] = defaultdict(list)
for i, line in enumerate(lines):
j = 0 j = 0
while j < len(line): while j < len(line):
# skip everything until a digit is found (start of a number) # skip everything until a digit is found (start of a number)
@ -44,10 +49,5 @@ for i, line in enumerate(lines):
# continue starting from the end of the number # continue starting from the end of the number
j = k j = k
# part 1 yield sum(values)
answer_1 = sum(values) yield sum(v1 * v2 for v1, v2 in filter(lambda vs: len(vs) == 2, gears.values()))
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = sum(v1 * v2 for v1, v2 in filter(lambda vs: len(vs) == 2, gears.values()))
print(f"answer 2 is {answer_2}")

View File

@ -1,5 +1,7 @@
import sys
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Iterator
from ..base import BaseSolver
@dataclass(frozen=True) @dataclass(frozen=True)
@ -9,10 +11,12 @@ class Card:
values: list[int] values: list[int]
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
cards: list[Card] = [] cards: list[Card] = []
for line in lines: for line in lines:
id_part, e_part = line.split(":") id_part, e_part = line.split(":")
numbers_s, values_s = e_part.split("|") numbers_s, values_s = e_part.split("|")
cards.append( cards.append(
@ -23,19 +27,18 @@ for line in lines:
) )
) )
winnings = [sum(1 for n in card.values if n in card.numbers) for card in cards] winnings = [sum(1 for n in card.values if n in card.numbers) for card in cards]
# part 1 # part 1
answer_1 = sum(2 ** (winning - 1) for winning in winnings if winning > 0) yield sum(2 ** (winning - 1) for winning in winnings if winning > 0)
print(f"answer 1 is {answer_1}")
# part 2 # part 2
card2cards = {i: list(range(i + 1, i + w + 1)) for i, w in enumerate(winnings)} card2cards = {i: list(range(i + 1, i + w + 1)) for i, w in enumerate(winnings)}
card2values = {i: 0 for i in range(len(cards))} card2values = {i: 0 for i in range(len(cards))}
for i in range(len(cards)): for i in range(len(cards)):
card2values[i] += 1 card2values[i] += 1
for j in card2cards[i]: for j in card2cards[i]:
card2values[j] += card2values[i] card2values[j] += card2values[i]
print(f"answer 2 is {sum(card2values.values())}") yield sum(card2values.values())

View File

@ -1,5 +1,6 @@
import sys from typing import Any, Iterator, Sequence
from typing import Sequence
from ..base import BaseSolver
MAP_ORDER = [ MAP_ORDER = [
"seed", "seed",
@ -12,55 +13,6 @@ MAP_ORDER = [
"location", "location",
] ]
lines = sys.stdin.read().splitlines()
# mappings from one category to another, each list contains
# ranges stored as (source, target, length), ordered by start and
# completed to have no "hole"
maps: dict[tuple[str, str], list[tuple[int, int, int]]] = {}
# parsing
index = 2
while index < len(lines):
p1, _, p2 = lines[index].split()[0].split("-")
# extract the existing ranges from the file - we store as (source, target, length)
# whereas the file is in order (target, source, length)
index += 1
values: list[tuple[int, int, int]] = []
while index < len(lines) and lines[index]:
n1, n2, n3 = lines[index].split()
values.append((int(n2), int(n1), int(n3)))
index += 1
# sort by source value
values.sort()
# add a 'fake' interval starting at 0 if missing
if values[0][0] != 0:
values.insert(0, (0, 0, values[0][0]))
# fill gaps between intervals
for i in range(len(values) - 1):
next_start = values[i + 1][0]
end = values[i][0] + values[i][2]
if next_start != end:
values.insert(
i + 1,
(end, end, next_start - end),
)
# add an interval covering values up to at least 2**32 at the end
last_start, _, last_length = values[-1]
values.append((last_start + last_length, last_start + last_length, 2**32))
assert all(v1[0] + v1[2] == v2[0] for v1, v2 in zip(values[:-1], values[1:]))
assert values[0][0] == 0
assert values[-1][0] + values[-1][-1] >= 2**32
maps[p1, p2] = values
index += 1
def find_range( def find_range(
values: tuple[int, int], map: list[tuple[int, int, int]] values: tuple[int, int], map: list[tuple[int, int, int]]
@ -111,19 +63,71 @@ def find_range(
return ranges return ranges
def find_location_ranges(seeds: Sequence[tuple[int, int]]) -> Sequence[tuple[int, int]]: class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
# mappings from one category to another, each list contains
# ranges stored as (source, target, length), ordered by start and
# completed to have no "hole"
maps: dict[tuple[str, str], list[tuple[int, int, int]]] = {}
def find_location_ranges(
seeds: Sequence[tuple[int, int]],
) -> Sequence[tuple[int, int]]:
for map1, map2 in zip(MAP_ORDER[:-1], MAP_ORDER[1:]): for map1, map2 in zip(MAP_ORDER[:-1], MAP_ORDER[1:]):
seeds = [s2 for s1 in seeds for s2 in find_range(s1, maps[map1, map2])] seeds = [s2 for s1 in seeds for s2 in find_range(s1, maps[map1, map2])]
return seeds return seeds
# parsing
index = 2
while index < len(lines):
p1, _, p2 = lines[index].split()[0].split("-")
# part 1 - use find_range() with range of length 1 # extract the existing ranges from the file - we store as (source, target, length)
seeds_p1 = [(int(s), 1) for s in lines[0].split(":")[1].strip().split()] # whereas the file is in order (target, source, length)
answer_1 = min(start for start, _ in find_location_ranges(seeds_p1)) index += 1
print(f"answer 1 is {answer_1}") values: list[tuple[int, int, int]] = []
while index < len(lines) and lines[index]:
n1, n2, n3 = lines[index].split()
values.append((int(n2), int(n1), int(n3)))
index += 1
# # part 2 # sort by source value
parts = lines[0].split(":")[1].strip().split() values.sort()
seeds_p2 = [(int(s), int(e)) for s, e in zip(parts[::2], parts[1::2])]
answer_2 = min(start for start, _ in find_location_ranges(seeds_p2)) # add a 'fake' interval starting at 0 if missing
print(f"answer 2 is {answer_2}") if values[0][0] != 0:
values.insert(0, (0, 0, values[0][0]))
# fill gaps between intervals
for i in range(len(values) - 1):
next_start = values[i + 1][0]
end = values[i][0] + values[i][2]
if next_start != end:
values.insert(
i + 1,
(end, end, next_start - end),
)
# add an interval covering values up to at least 2**32 at the end
last_start, _, last_length = values[-1]
values.append((last_start + last_length, last_start + last_length, 2**32))
assert all(
v1[0] + v1[2] == v2[0] for v1, v2 in zip(values[:-1], values[1:])
)
assert values[0][0] == 0
assert values[-1][0] + values[-1][-1] >= 2**32
maps[p1, p2] = values
index += 1
# part 1 - use find_range() with range of length 1
seeds_p1 = [(int(s), 1) for s in lines[0].split(":")[1].strip().split()]
yield min(start for start, _ in find_location_ranges(seeds_p1))
# # part 2
parts = lines[0].split(":")[1].strip().split()
seeds_p2 = [(int(s), int(e)) for s, e in zip(parts[::2], parts[1::2])]
yield min(start for start, _ in find_location_ranges(seeds_p2))

View File

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

View File

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

View File

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

View File

@ -1,15 +1,22 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
data = [[int(c) for c in line.split()] for line in lines]
right_values: list[int] = [] class Solver(BaseSolver):
left_values: list[int] = [] def solve(self, input: str) -> Iterator[Any]:
for values in data: lines = input.splitlines()
data = [[int(c) for c in line.split()] for line in lines]
right_values: list[int] = []
left_values: list[int] = []
for values in data:
diffs = [values] diffs = [values]
while any(d != 0 for d in diffs[-1]): while any(d != 0 for d in diffs[-1]):
diffs.append([rhs - lhs for lhs, rhs in zip(diffs[-1][:-1], diffs[-1][1:])]) diffs.append(
[rhs - lhs for lhs, rhs in zip(diffs[-1][:-1], diffs[-1][1:])]
)
rhs: list[int] = [0] rhs: list[int] = [0]
lhs: list[int] = [0] lhs: list[int] = [0]
@ -20,10 +27,8 @@ for values in data:
right_values.append(rhs[-1]) right_values.append(rhs[-1])
left_values.append(lhs[-1]) left_values.append(lhs[-1])
# part 1 # part 1
answer_1 = sum(right_values) yield sum(right_values)
print(f"answer 1 is {answer_1}")
# part 2 # part 2
answer_2 = sum(left_values) yield sum(left_values)
print(f"answer 2 is {answer_2}")

View File

@ -1,14 +1,17 @@
import sys
from collections import Counter from collections import Counter
from typing import Any, Iterator
values = list(map(int, sys.stdin.read().strip().split())) from ..base import BaseSolver
column_1 = sorted(values[::2])
column_2 = sorted(values[1::2])
counter_2 = Counter(column_2)
answer_1 = sum(abs(v1 - v2) for v1, v2 in zip(column_1, column_2, strict=True)) class Solver(BaseSolver):
answer_2 = sum(value * counter_2.get(value, 0) for value in column_1) def solve(self, input: str) -> Iterator[Any]:
values = list(map(int, input.split()))
print(f"answer 1 is {answer_1}") column_1 = sorted(values[::2])
print(f"answer 2 is {answer_2}") column_2 = sorted(values[1::2])
yield sum(abs(v1 - v2) for v1, v2 in zip(column_1, column_2, strict=True))
counter_2 = Counter(column_2)
yield sum(value * counter_2.get(value, 0) for value in column_1)

Some files were not shown because too many files have changed in this diff Show More