2 Commits

Author SHA1 Message Date
Mikael CAPELLE
b21c50562f Refactor 2024 day 3.
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-03 15:22:37 +01:00
Mikael CAPELLE
211483f679 Add .drone.yml for CI.
All checks were successful
continuous-integration/drone Build is passing
2024-12-03 14:12:42 +01:00
111 changed files with 2440 additions and 5609 deletions

16
poetry.lock generated
View File

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

View File

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

View File

@@ -1,12 +1,10 @@
from typing import Any, Iterator
import sys
from ..base import BaseSolver
line = sys.stdin.read().strip()
floor = 0
floors = [(floor := floor + (1 if c == "(" else -1)) for c in line]
class Solver(BaseSolver):
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)
print(f"answer 1 is {floors[-1]}")
print(f"answer 2 is {floors.index(-1)}")

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
import sys
from dataclasses import dataclass
from typing import Any, Iterator, Literal, cast
from typing import Literal, cast
import parse # type: ignore
from ..base import BaseSolver
@dataclass(frozen=True)
class Reindeer:
@@ -14,50 +13,50 @@ class Reindeer:
rest_time: int
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
lines = sys.stdin.read().splitlines()
reindeers: list[Reindeer] = []
for line in lines:
reindeer, speed, speed_time, rest_time = cast(
tuple[str, int, int, int],
parse.parse( # type: ignore
"{} can fly {:d} km/s for {:d} seconds, "
"but then must rest for {:d} seconds.",
line,
),
)
reindeers.append(
Reindeer(
name=reindeer, speed=speed, fly_time=speed_time, rest_time=rest_time
)
)
reindeers: list[Reindeer] = []
for line in lines:
reindeer, speed, speed_time, rest_time = cast(
tuple[str, int, int, int],
parse.parse( # type: ignore
"{} can fly {:d} km/s for {:d} seconds, "
"but then must rest for {:d} seconds.",
line,
),
)
reindeers.append(
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]] = {
reindeer: ("resting", 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}
states: dict[Reindeer, tuple[Literal["resting", "flying"], int]] = {
reindeer: ("resting", 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}
for time in self.progress.wrap(range(target)):
for reindeer in reindeers:
if states[reindeer][0] == "flying":
distances[reindeer] += reindeer.speed
for time in range(target):
for reindeer in reindeers:
if states[reindeer][0] == "flying":
distances[reindeer] += reindeer.speed
top_distance = max(distances.values())
for reindeer in reindeers:
if distances[reindeer] == top_distance:
points[reindeer] += 1
top_distance = max(distances.values())
for reindeer in reindeers:
if distances[reindeer] == top_distance:
points[reindeer] += 1
for reindeer in reindeers:
if states[reindeer][1] == time:
if states[reindeer][0] == "resting":
states[reindeer] = ("flying", time + reindeer.fly_time)
else:
states[reindeer] = ("resting", time + reindeer.rest_time)
for reindeer in reindeers:
if states[reindeer][1] == time:
if states[reindeer][0] == "resting":
states[reindeer] = ("flying", time + reindeer.fly_time)
else:
states[reindeer] = ("resting", time + reindeer.rest_time)
yield max(distances.values())
yield max(points.values()) - 1
answer_1 = max(distances.values())
print(f"answer 1 is {answer_1}")
answer_2 = max(points.values()) - 1
print(f"answer 2 is {answer_2}")

View File

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

View File

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

View File

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

View File

@@ -1,66 +1,66 @@
import itertools
from typing import Any, Iterator
import sys
import numpy as np
from numpy.typing import NDArray
from ..base import BaseSolver
grid0 = np.array([[c == "#" for c in line] for line in sys.stdin.read().splitlines()])
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(
# add an always off circle around
grid0 = np.concatenate(
[
np.zeros((grid0.shape[0] + 2, 1), dtype=bool),
np.concatenate(
[
np.zeros((grid0.shape[0] + 2, 1), dtype=bool),
np.concatenate(
[
np.zeros((1, grid0.shape[1]), dtype=bool),
grid0,
np.zeros((1, grid0.shape[1]), dtype=bool),
]
),
np.zeros((grid0.shape[0] + 2, 1), dtype=bool),
],
axis=1,
)
np.zeros((1, grid0.shape[1]), dtype=bool),
grid0,
np.zeros((1, grid0.shape[1]), dtype=bool),
]
),
np.zeros((grid0.shape[0] + 2, 1), dtype=bool),
],
axis=1,
)
moves = list(itertools.product([-1, 0, 1], repeat=2))
moves.remove((0, 0))
moves = list(itertools.product([-1, 0, 1], repeat=2))
moves.remove((0, 0))
jjs, iis = np.meshgrid(
np.arange(1, grid0.shape[0] - 1, dtype=int),
np.arange(1, grid0.shape[1] - 1, dtype=int),
)
iis, jjs = iis.flatten(), jjs.flatten()
jjs, iis = np.meshgrid(
np.arange(1, grid0.shape[0] - 1, dtype=int),
np.arange(1, grid0.shape[1] - 1, dtype=int),
)
iis, jjs = iis.flatten(), jjs.flatten()
ins = iis[:, None] + np.array(moves)[:, 0]
jns = jjs[:, None] + np.array(moves)[:, 1]
ins = iis[:, None] + np.array(moves)[:, 0]
jns = jjs[:, None] + np.array(moves)[:, 1]
def game_of_life(grid: NDArray[np.bool_]) -> NDArray[np.bool_]:
neighbors_on = grid[ins, jns].sum(axis=1)
cells_on = grid[iis, jjs]
grid = np.zeros_like(grid)
grid[iis, jjs] = (neighbors_on == 3) | (cells_on & (neighbors_on == 2))
def game_of_life(grid: NDArray[np.bool_]) -> NDArray[np.bool_]:
neighbors_on = grid[ins, jns].sum(axis=1)
cells_on = grid[iis, jjs]
return grid
grid = np.zeros_like(grid)
grid[iis, jjs] = (neighbors_on == 3) | (cells_on & (neighbors_on == 2))
grid = grid0
n_steps = 4 if len(grid) < 10 else 100
for _ in range(n_steps):
grid = game_of_life(grid)
return grid
yield grid.sum()
n_steps = 5 if len(grid) < 10 else 100
grid = grid0
for _ in range(n_steps):
grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True
grid = game_of_life(grid)
grid = grid0
n_steps = 4 if len(grid) < 10 else 100
for _ in range(n_steps):
grid = game_of_life(grid)
grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True
answer_1 = grid.sum()
print(f"answer 1 is {answer_1}")
yield sum(cell for line in grid for cell in line)
n_steps = 5 if len(grid) < 10 else 100
grid = grid0
for _ in range(n_steps):
grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True
grid = game_of_life(grid)
grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True
answer_2 = sum(cell for line in grid for cell in line)
print(f"answer 2 is {answer_2}")

View File

@@ -1,58 +1,56 @@
import sys
from collections import defaultdict
from typing import Any, Iterator
from ..base import BaseSolver
replacements_s, molecule = sys.stdin.read().split("\n\n")
REPLACEMENTS: dict[str, list[str]] = defaultdict(list)
for replacement_s in replacements_s.splitlines():
p = replacement_s.split(" => ")
REPLACEMENTS[p[0]].append(p[1])
molecule = molecule.strip()
generated = [
molecule[:i] + replacement + molecule[i + len(symbol) :]
for symbol, replacements in REPLACEMENTS.items()
for replacement in replacements
for i in range(len(molecule))
if molecule[i:].startswith(symbol)
]
answer_1 = len(set(generated))
print(f"answer 1 is {answer_1}")
inversion: dict[str, str] = {
replacement: symbol
for symbol, replacements in REPLACEMENTS.items()
for replacement in replacements
}
# 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
# without thinking
count = 0
while molecule != "e":
i = 0
m2 = ""
while i < len(molecule):
found = False
for replacement in inversion:
if molecule[i:].startswith(replacement):
m2 += inversion[replacement]
i += len(replacement)
count += 1
found = True
break
if not found:
m2 += molecule[i]
i += 1
# print(m2)
molecule = m2
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(" => ")
REPLACEMENTS[p[0]].append(p[1])
molecule = molecule.strip()
generated = [
molecule[:i] + replacement + molecule[i + len(symbol) :]
for symbol, replacements in REPLACEMENTS.items()
for replacement in replacements
for i in range(len(molecule))
if molecule[i:].startswith(symbol)
]
yield len(set(generated))
inversion: dict[str, str] = {
replacement: symbol
for symbol, replacements in REPLACEMENTS.items()
for replacement in replacements
}
# 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
# without thinking
count = 0
while molecule != "e":
i = 0
m2 = ""
while i < len(molecule):
found = False
for replacement in inversion:
if molecule[i:].startswith(replacement):
m2 += inversion[replacement]
i += len(replacement)
count += 1
found = True
break
if not found:
m2 += molecule[i]
i += 1
# print(m2)
molecule = m2
yield count
answer_2 = count
print(f"answer 2 is {count}")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,32 +1,33 @@
from typing import Any, Iterator, Literal, cast
import sys
from typing import Literal, cast
import numpy as np
import parse # type: ignore
from ..base import BaseSolver
lines = sys.stdin.read().splitlines()
lights_1 = np.zeros((1000, 1000), dtype=bool)
lights_2 = np.zeros((1000, 1000), dtype=int)
for line in lines:
action, sx, sy, ex, ey = cast(
tuple[Literal["turn on", "turn off", "toggle"], int, int, int, int],
parse.parse("{} {:d},{:d} through {:d},{:d}", line), # type: ignore
)
ex, ey = ex + 1, ey + 1
class Solver(BaseSolver):
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(
tuple[Literal["turn on", "turn off", "toggle"], int, int, int, int],
parse.parse("{} {:d},{:d} through {:d},{:d}", line), # type: ignore
)
ex, ey = ex + 1, ey + 1
match action:
case "turn on":
lights_1[sx:ex, sy:ey] = True
lights_2[sx:ex, sy:ey] += 1
case "turn off":
lights_1[sx:ex, sy:ey] = False
lights_2[sx:ex, sy:ey] = np.maximum(lights_2[sx:ex, sy:ey] - 1, 0)
case "toggle":
lights_1[sx:ex, sy:ey] = ~lights_1[sx:ex, sy:ey]
lights_2[sx:ex, sy:ey] += 2
match action:
case "turn on":
lights_1[sx:ex, sy:ey] = True
lights_2[sx:ex, sy:ey] += 1
case "turn off":
lights_1[sx:ex, sy:ey] = False
lights_2[sx:ex, sy:ey] = np.maximum(lights_2[sx:ex, sy:ey] - 1, 0)
case "toggle":
lights_1[sx:ex, sy:ey] = ~lights_1[sx:ex, sy:ey]
lights_2[sx:ex, sy:ey] += 2
answer_1 = lights_1.sum()
print(f"answer 1 is {answer_1}")
yield lights_1.sum()
yield lights_2.sum()
answer_2 = lights_2.sum()
print(f"answer 2 is {answer_2}")

View File

@@ -1,7 +1,11 @@
import logging
import operator
from typing import Any, Callable, Iterator
import os
import sys
from typing import Callable
from ..base import BaseSolver
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
OPERATORS = {
"AND": operator.and_,
@@ -32,6 +36,48 @@ def value_of(key: str) -> tuple[str, Callable[[dict[str, int]], int]]:
return key, lambda values: values[key]
lines = sys.stdin.read().splitlines()
signals: Signals = {}
values: dict[str, int] = {"": 0}
for line in lines:
command, signal = line.split(" -> ")
if command.startswith("NOT"):
name = command.split(" ")[1]
signals[signal] = (
(name, ""),
(lambda values, _n=name: values[_n], lambda _v: 0),
lambda a, _b: ~a,
)
elif not any(command.find(name) >= 0 for name in OPERATORS):
try:
values[signal] = int(command)
except ValueError:
signals[signal] = (
(command, ""),
(lambda values, _c=command: values[_c], lambda _v: 0),
lambda a, _b: a,
)
else:
op: Callable[[int, int], int] = zero_op
lhs_s, rhs_s = "", ""
for name in OPERATORS:
if command.find(name) >= 0:
op = OPERATORS[name]
lhs_s, rhs_s = command.split(f" {name} ")
break
lhs_s, lhs_fn = value_of(lhs_s)
rhs_s, rhs_fn = value_of(rhs_s)
signals[signal] = ((lhs_s, rhs_s), (lhs_fn, rhs_fn), op)
def process(
signals: Signals,
values: dict[str, int],
@@ -45,52 +91,11 @@ def process(
return values
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any] | None:
lines = input.splitlines()
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}")
signals: Signals = {}
values: dict[str, int] = {"": 0}
for line in lines:
command, signal = line.split(" -> ")
if command.startswith("NOT"):
name = command.split(" ")[1]
signals[signal] = (
(name, ""),
(lambda values, _n=name: values[_n], lambda _v: 0),
lambda a, _b: ~a,
)
elif not any(command.find(name) >= 0 for name in OPERATORS):
try:
values[signal] = int(command)
except ValueError:
signals[signal] = (
(command, ""),
(lambda values, _c=command: values[_c], lambda _v: 0),
lambda a, _b: a,
)
else:
op: Callable[[int, int], int] = zero_op
lhs_s, rhs_s = "", ""
for name in OPERATORS:
if command.find(name) >= 0:
op = OPERATORS[name]
lhs_s, rhs_s = command.split(f" {name} ")
break
lhs_s, lhs_fn = value_of(lhs_s)
rhs_s, rhs_fn = value_of(rhs_s)
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"]
yield process(signals.copy(), values | {"b": values_1["a"]})["a"]
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,32 +1,35 @@
from typing import Any, Iterator
import logging
import os
import sys
from ..base import BaseSolver
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
lines = sys.stdin.read().splitlines()
yield sum(
# left and right quotes (not in memory)
2
# each \\ adds one character in the literals (compared to memory)
+ line.count(R"\\")
# each \" adds one character in the literals (compared to memory)
+ line[1:-1].count(R"\"")
# each \xFF adds 3 characters in the literals (compared to memory), but we must not
# count A\\x (A != \), but we must count A\\\x (A != \) - in practice we should also
# 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"))
for line in lines
)
answer_1 = sum(
# left and right quotes (not in memory)
2
# each \\ adds one character in the literals (compared to memory)
+ line.count(R"\\")
# each \" adds one character in the literals (compared to memory)
+ line[1:-1].count(R"\"")
# each \xFF adds 3 characters in the literals (compared to memory), but we must not
# count A\\x (A != \), but we must count A\\\x (A != \) - in practice we should also
# 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"))
for line in lines
)
print(f"answer 1 is {answer_1}")
yield sum(
# needs to wrap in quotes (2 characters)
2
# needs to escape every \ with an extra \
+ line.count("\\")
# needs to escape every " with an extra \ (including the first and last ones)
+ line.count('"')
for line in lines
)
answer_2 = sum(
# needs to wrap in quotes (2 characters)
2
# needs to escape every \ with an extra \
+ line.count("\\")
# needs to escape every " with an extra \ (including the first and last ones)
+ line.count('"')
for line in lines
)
print(f"answer 2 is {answer_2}")

View File

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

View File

@@ -1,12 +1,7 @@
from typing import Any, Iterator
import sys
from ..base import BaseSolver
blocks = sys.stdin.read().split("\n\n")
values = sorted(sum(map(int, block.split())) for block in blocks)
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:])
print(f"answer 1 is {values[-1]}")
print(f"answer 2 is {sum(values[-3:])}")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,96 +1,90 @@
from typing import Any, Iterator
import sys
from typing import Any
import numpy as np
import parse # type: ignore
from numpy.typing import NDArray
from ..base import BaseSolver
def part1(sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], row: int) -> int:
no_beacons_row_l: list[NDArray[np.floating[Any]]] = []
for (sx, sy), (bx, by) in sensor_to_beacon.items():
d = abs(sx - bx) + abs(sy - by) # closest
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)
no_beacons_row = set(np.concatenate(no_beacons_row_l)).difference(beacons_at_row) # type: ignore
return len(no_beacons_row)
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]]] = []
for (sx, sy), (bx, by) in sensor_to_beacon.items():
d = abs(sx - bx) + abs(sy - by) # closest
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)
no_beacons_row = set(np.concatenate(no_beacons_row_l)).difference(
beacons_at_row
) # type: ignore
return len(no_beacons_row)
def part2_intervals(
self, sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int
) -> tuple[int, int, int]:
for y in self.progress.wrap(range(xy_max + 1)):
its: list[tuple[int, int]] = []
for (sx, sy), (bx, by) in sensor_to_beacon.items():
d = abs(sx - bx) + abs(sy - by)
dx = d - abs(sy - y)
if dx >= 0:
its.append((max(0, sx - dx), min(sx + dx, xy_max)))
its = sorted(its)
_, e = its[0]
for si, ei in its[1:]:
if si > e + 1:
return si - 1, y, 4_000_000 * (si - 1) + y
if ei > e:
e = ei
return (0, 0, 0)
def part2_cplex(
self, sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int
) -> tuple[int, int, int]:
from docplex.mp.model import Model
m = Model()
x, y = m.continuous_var_list(2, ub=xy_max, name=["x", "y"])
def part2_intervals(
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int
) -> tuple[int, int, int]:
from tqdm import trange
for y in trange(xy_max + 1):
its: list[tuple[int, int]] = []
for (sx, sy), (bx, by) in sensor_to_beacon.items():
d = abs(sx - bx) + abs(sy - by)
m.add_constraint(
m.abs(x - sx) + m.abs(y - sy) >= d + 1, # type: ignore
ctname=f"ct_{sx}_{sy}",
)
dx = d - abs(sy - y)
m.set_objective("min", x + y)
if dx >= 0:
its.append((max(0, sx - dx), min(sx + dx, xy_max)))
s = m.solve()
assert s is not None
its = sorted(its)
_, e = its[0]
vx = int(s.get_value(x))
vy = int(s.get_value(y))
return vx, vy, 4_000_000 * vx + vy
for si, ei in its[1:]:
if si > e + 1:
return si - 1, y, 4_000_000 * (si - 1) + y
if ei > e:
e = ei
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
return (0, 0, 0)
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]] = {}
for line in lines:
r: dict[str, str] = parse.parse( # type: ignore
"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"]))
def part2_cplex(
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int
) -> tuple[int, int, int]:
from docplex.mp.model import Model
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
m = Model()
yield self.part1(sensor_to_beacon, row)
x, y = m.continuous_var_list(2, ub=xy_max, name=["x", "y"])
# x, y, a2 = part2_cplex(sensor_to_beacon, xy_max)
x, y, a2 = self.part2_intervals(sensor_to_beacon, xy_max)
self.logger.info("answer 2 is {at} (x={x}, y={y})")
yield a2
for (sx, sy), (bx, by) in sensor_to_beacon.items():
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.set_objective("min", x + y)
s = m.solve()
assert s is not None
vx = int(s.get_value(x))
vy = int(s.get_value(y))
return vx, vy, 4_000_000 * vx + vy
lines = sys.stdin.read().splitlines()
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]] = {}
for line in lines:
r: dict[str, str] = parse.parse( # type: ignore
"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"]))
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
print(f"answer 1 is {part1(sensor_to_beacon, row)}")
# x, y, a2 = part2_cplex(sensor_to_beacon, xy_max)
x, y, a2 = part2_intervals(sensor_to_beacon, xy_max)
print(f"answer 2 is {a2} (x={x}, y={y})")

View File

@@ -5,12 +5,10 @@ import itertools
import re
import sys
from collections import defaultdict
from typing import Any, FrozenSet, Iterator, NamedTuple
from typing import FrozenSet, NamedTuple
from tqdm import tqdm
from ..base import BaseSolver
class Pipe(NamedTuple):
name: str

View File

@@ -1,10 +1,8 @@
import sys
from typing import Any, Iterator, Sequence, TypeVar
from typing import Sequence, TypeVar
import numpy as np
from ..base import BaseSolver
T = TypeVar("T")

View File

@@ -1,10 +1,7 @@
import sys
from typing import Any, Iterator
import numpy as np
from ..base import BaseSolver
xyz = np.asarray(
[
tuple(int(x) for x in row.split(",")) # type: ignore

View File

@@ -1,12 +1,10 @@
import sys
from typing import Any, Iterator, Literal
from typing import Any, Literal
import numpy as np
import parse # pyright: ignore[reportMissingTypeStubs]
from numpy.typing import NDArray
from ..base import BaseSolver
Reagent = Literal["ore", "clay", "obsidian", "geode"]
REAGENTS: tuple[Reagent, ...] = (
"ore",

View File

@@ -1,6 +1,4 @@
from typing import Any, Iterator
from ..base import BaseSolver
import sys
def score_1(ux: int, vx: int) -> int:
@@ -35,23 +33,21 @@ def score_2(ux: int, vx: int) -> int:
return (ux + vx - 1) % 3 + 1 + vx * 3
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
lines = sys.stdin.readlines()
# the solution relies on replacing rock / paper / scissor by values 0 / 1 / 2 and using
# modulo-3 arithmetic
#
# 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)))
#
# the solution relies on replacing rock / paper / scissor by values 0 / 1 / 2 and using
# modulo-3 arithmetic
#
# 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)))
#
# 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
values = [(ord(row[0]) - ord("A"), ord(row[2]) - ord("X")) for row in lines]
# 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
values = [(ord(row[0]) - ord("A"), ord(row[2]) - ord("X")) for row in lines]
# part 1 - 13526
yield sum(score_1(*v) for v in values)
# part 1 - 13526
print(f"answer 1 is {sum(score_1(*v) for v in values)}")
# part 2 - 14204
yield sum(score_2(*v) for v in values)
# part 2 - 14204
print(f"answer 2 is {sum(score_2(*v) for v in values)}")

View File

@@ -1,9 +1,6 @@
from __future__ import annotations
import sys
from typing import Any, Iterator
from ..base import BaseSolver
class Number:

View File

@@ -1,8 +1,6 @@
import operator
import sys
from typing import Any, Callable, Iterator
from ..base import BaseSolver
from typing import Callable
def compute(monkeys: dict[str, int | tuple[str, str, str]], monkey: str) -> int:

View File

@@ -1,11 +1,9 @@
import re
import sys
from typing import Any, Callable, Iterator
from typing import Callable
import numpy as np
from ..base import BaseSolver
VOID, EMPTY, WALL = 0, 1, 2
TILE_FROM_CHAR = {" ": VOID, ".": EMPTY, "#": WALL}

View File

@@ -1,9 +1,6 @@
import itertools
import sys
from collections import defaultdict
from typing import Any, Iterator
from ..base import BaseSolver
Directions = list[
tuple[

View File

@@ -2,9 +2,6 @@ import heapq
import math
import sys
from collections import defaultdict
from typing import Any, Iterator
from ..base import BaseSolver
lines = sys.stdin.read().splitlines()

View File

@@ -1,7 +1,4 @@
import sys
from typing import Any, Iterator
from ..base import BaseSolver
lines = sys.stdin.read().splitlines()

View File

@@ -1,28 +1,23 @@
import string
from typing import Any, Iterator
import sys
from ..base import BaseSolver
lines = [line.strip() for line in sys.stdin.readlines()]
# extract content of each part
parts = [(set(line[: len(line) // 2]), set(line[len(line) // 2 :])) for line in lines]
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
# priorities
priorities = {c: i + 1 for i, c in enumerate(string.ascii_letters)}
# extract content of each part
parts = [
(set(line[: len(line) // 2]), set(line[len(line) // 2 :])) for line in lines
]
# part 1
part1 = sum(priorities[c] for p1, p2 in parts for c in p1.intersection(p2))
print(f"answer 1 is {part1}")
# priorities
priorities = {c: i + 1 for i, c in enumerate(string.ascii_letters)}
# 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]
for i in range(0, len(lines), n_per_group)
for c in set(lines[i]).intersection(*lines[i + 1 : i + n_per_group])
)
# part 2
n_per_group = 3
part2 = sum(
priorities[c]
for i in range(0, len(lines), 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 @@
from typing import Any, Iterator
import sys
from ..base import BaseSolver
lines = [line.strip() for line in sys.stdin.readlines()]
def make_range(value: str) -> set[int]:
@@ -8,13 +8,10 @@ def make_range(value: str) -> set[int]:
return set(range(int(parts[0]), int(parts[1]) + 1))
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
sections = [tuple(make_range(part) for part in line.split(",")) for line in lines]
sections = [
tuple(make_range(part) for part in line.split(",")) for line in lines
]
answer_1 = sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections)
print(f"answer 1 is {answer_1}")
yield sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections)
yield sum(bool(s1.intersection(s2)) for s1, s2 in sections)
answer_2 = sum(bool(s1.intersection(s2)) for s1, s2 in sections)
print(f"answer 1 is {answer_2}")

View File

@@ -1,43 +1,41 @@
import copy
from typing import Any, Iterator
import sys
from ..base import BaseSolver
blocks_s, moves_s = (part.splitlines() for part in sys.stdin.read().split("\n\n"))
blocks: dict[str, list[str]] = {stack: [] for stack in blocks_s[-1].split()}
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
blocks_s, moves_s = (part.splitlines() for part in input.split("\n\n"))
# 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)):
crate = block[index + 1 : index + 2].strip()
blocks: dict[str, list[str]] = {stack: [] for stack in blocks_s[-1].split()}
if crate:
blocks[stack].append(crate)
# 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)):
crate = block[index + 1 : index + 2].strip()
# part 1 - deep copy for part 2
blocks_1 = copy.deepcopy(blocks)
if crate:
blocks[stack].append(crate)
for move in moves_s:
_, count_s, _, from_, _, to_ = move.strip().split()
# part 1 - deep copy for part 2
blocks_1 = copy.deepcopy(blocks)
for _i in range(int(count_s)):
blocks_1[to_].append(blocks_1[from_].pop())
for move in moves_s:
_, count_s, _, from_, _, to_ = move.strip().split()
# part 2
blocks_2 = copy.deepcopy(blocks)
for _i in range(int(count_s)):
blocks_1[to_].append(blocks_1[from_].pop())
for move in moves_s:
_, count_s, _, from_, _, to_ = move.strip().split()
count = int(count_s)
# part 2
blocks_2 = copy.deepcopy(blocks)
blocks_2[to_].extend(blocks_2[from_][-count:])
del blocks_2[from_][-count:]
for move in moves_s:
_, count_s, _, from_, _, to_ = move.strip().split()
count = int(count_s)
answer_1 = "".join(s[-1] for s in blocks_1.values())
print(f"answer 1 is {answer_1}")
blocks_2[to_].extend(blocks_2[from_][-count:])
del blocks_2[from_][-count:]
yield "".join(s[-1] for s in blocks_1.values())
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,6 +1,4 @@
from typing import Any, Iterator
from ..base import BaseSolver
import sys
def index_of_first_n_differents(data: str, n: int) -> int:
@@ -10,7 +8,8 @@ def index_of_first_n_differents(data: str, n: int) -> int:
return -1
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
yield index_of_first_n_differents(input, 4)
yield index_of_first_n_differents(input, 14)
data = sys.stdin.read().strip()
print(f"answer 1 is {index_of_first_n_differents(data, 4)}")
print(f"answer 2 is {index_of_first_n_differents(data, 14)}")

View File

@@ -1,81 +1,80 @@
import sys
from pathlib import Path
from typing import Any, Iterator
from ..base import BaseSolver
lines = sys.stdin.read().splitlines()
# 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
trees: dict[Path, list[Path]] = {}
# mapping from paths to either size (for file) or -1 for directory
sizes: dict[Path, int] = {}
# 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
if line.startswith("$"):
parts = line.strip("$").strip().split()
command = parts[0]
if command == "cd":
cur_path = cur_path.joinpath(parts[1]).resolve()
# just initialize the lis of files if not already done
if cur_path not in trees:
trees[cur_path] = []
else:
# nothing to do here
pass
# fill the current path
else:
parts = line.split()
name: str = parts[1]
if line.startswith("dir"):
size = -1
else:
size = int(parts[0])
path = cur_path.joinpath(name)
trees[cur_path].append(path)
sizes[path] = size
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
def compute_size(path: Path) -> int:
size = sizes[path]
# 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
#
if size >= 0:
return size
# mapping from path to list of files or directories
trees: dict[Path, list[Path]] = {}
return sum(compute_size(sub) for sub in trees[path])
# mapping from paths to either size (for file) or -1 for directory
sizes: dict[Path, int] = {}
# 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
acc_sizes = {path: compute_size(path) for path in trees}
trees[cur_path] = []
sizes[cur_path] = -1
# part 1
answer_1 = sum(size for size in acc_sizes.values() if size <= 100_000)
print(f"answer 1 is {answer_1}")
for line in lines[1:]:
# command
if line.startswith("$"):
parts = line.strip("$").strip().split()
command = parts[0]
# part 2
total_space = 70_000_000
update_space = 30_000_000
free_space = total_space - acc_sizes[base_path]
if command == "cd":
cur_path = cur_path.joinpath(parts[1]).resolve()
to_free_space = update_space - free_space
# just initialize the lis of files if not already done
if cur_path not in trees:
trees[cur_path] = []
else:
# nothing to do here
pass
# fill the current path
else:
parts = line.split()
name: str = parts[1]
if line.startswith("dir"):
size = -1
else:
size = int(parts[0])
path = cur_path.joinpath(name)
trees[cur_path].append(path)
sizes[path] = size
def compute_size(path: Path) -> int:
size = sizes[path]
if size >= 0:
return size
return sum(compute_size(sub) for sub in trees[path])
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 2
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,54 +1,53 @@
from typing import Any, Iterator
import sys
import numpy as np
from numpy.typing import NDArray
from ..base import BaseSolver
lines = sys.stdin.read().splitlines()
trees = np.array([[int(x) for x in row] for row in lines])
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
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 + 1 :, j].max(),
trees[i, :j].max(),
trees[i, j + 1 :].max(),
]
for j in range(1, trees.shape[1] - 1)
]
for i in range(1, trees.shape[0] - 1)
# answer 1
highest_trees = np.ones(trees.shape + (4,), dtype=int) * -1
highest_trees[1:-1, 1:-1] = [
[
[
trees[:i, j].max(),
trees[i + 1 :, j].max(),
trees[i, :j].max(),
trees[i, j + 1 :].max(),
]
for j in range(1, trees.shape[1] - 1)
]
for i in range(1, trees.shape[0] - 1)
]
yield (highest_trees.min(axis=2) < trees).sum()
answer_1 = (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:
w = np.where(row_of_trees >= value)[0]
if not w.size:
return len(row_of_trees)
def viewing_distance(row_of_trees: NDArray[np.int_], value: int) -> int:
w = np.where(row_of_trees >= value)[0]
return w[0] + 1
if not w.size:
return len(row_of_trees)
# answer 2
v_distances = np.zeros(trees.shape + (4,), dtype=int)
v_distances[1:-1, 1:-1, :] = [
[
[
viewing_distance(trees[i - 1 :: -1, j], trees[i, j]),
viewing_distance(trees[i, j - 1 :: -1], trees[i, j]),
viewing_distance(trees[i, j + 1 :], trees[i, j]),
viewing_distance(trees[i + 1 :, j], trees[i, j]),
]
for j in range(1, trees.shape[1] - 1)
]
for i in range(1, trees.shape[0] - 1)
return w[0] + 1
# answer 2
v_distances = np.zeros(trees.shape + (4,), dtype=int)
v_distances[1:-1, 1:-1, :] = [
[
[
viewing_distance(trees[i - 1 :: -1, j], trees[i, j]),
viewing_distance(trees[i, j - 1 :: -1], trees[i, j]),
viewing_distance(trees[i, j + 1 :], trees[i, j]),
viewing_distance(trees[i + 1 :, j], trees[i, j]),
]
yield np.prod(v_distances, axis=2).max()
for j in range(1, trees.shape[1] - 1)
]
for i in range(1, trees.shape[0] - 1)
]
answer_2 = np.prod(v_distances, axis=2).max()
print(f"answer 2 is {answer_2}")

View File

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

View File

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

View File

@@ -1,42 +1,41 @@
from typing import Any, Iterator
import sys
import numpy as np
from ..base import BaseSolver
lines = sys.stdin.read().splitlines()
data = np.array([[c == "#" for c in line] for line in lines])
rows = {c for c in range(data.shape[0]) if not data[c, :].any()}
columns = {c for c in range(data.shape[1]) if not data[:, c].any()}
galaxies_y, galaxies_x = np.where(data) # type: ignore
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
def compute_total_distance(expansion: int) -> int:
distances: list[int] = []
for g1 in range(len(galaxies_y)):
x1, y1 = int(galaxies_x[g1]), int(galaxies_y[g1])
for g2 in range(g1 + 1, len(galaxies_y)):
x2, y2 = int(galaxies_x[g2]), int(galaxies_y[g2])
data = np.array([[c == "#" for c in line] for line in lines])
dx = sum(
1 + (expansion - 1) * (x in columns)
for x in range(min(x1, x2), max(x1, x2))
)
dy = sum(
1 + (expansion - 1) * (y in rows)
for y in range(min(y1, y2), max(y1, y2))
)
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()}
distances.append(dx + dy)
return sum(distances)
galaxies_y, galaxies_x = np.where(data) # type: ignore
def compute_total_distance(expansion: int) -> int:
distances: list[int] = []
for g1 in range(len(galaxies_y)):
x1, y1 = int(galaxies_x[g1]), int(galaxies_y[g1])
for g2 in range(g1 + 1, len(galaxies_y)):
x2, y2 = int(galaxies_x[g2]), int(galaxies_y[g2])
# part 1
answer_1 = compute_total_distance(2)
print(f"answer 1 is {answer_1}")
dx = sum(
1 + (expansion - 1) * (x in columns)
for x in range(min(x1, x2), max(x1, x2))
)
dy = sum(
1 + (expansion - 1) * (y in rows)
for y in range(min(y1, y2), max(y1, y2))
)
distances.append(dx + dy)
return sum(distances)
# part 1
yield compute_total_distance(2)
# part 2
yield compute_total_distance(1000000)
# part 2
answer_2 = compute_total_distance(1000000)
print(f"answer 2 is {answer_2}")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,13 @@
from __future__ import annotations
import heapq
import os
import sys
from collections import defaultdict
from dataclasses import dataclass
from typing import Any, Iterator, Literal, TypeAlias
from typing import Literal, TypeAlias
from ..base import BaseSolver
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
Direction: TypeAlias = Literal[">", "<", "^", "v"]
@@ -30,204 +32,202 @@ MAPPINGS: dict[Direction, tuple[int, int, Direction]] = {
}
class Solver(BaseSolver):
def print_shortest_path(
self,
grid: list[list[int]],
target: tuple[int, int],
per_cell: dict[tuple[int, int], list[tuple[Label, int]]],
):
assert len(per_cell[target]) == 1
label = per_cell[target][0][0]
def print_shortest_path(
grid: list[list[int]],
target: tuple[int, int],
per_cell: dict[tuple[int, int], list[tuple[Label, int]]],
):
assert len(per_cell[target]) == 1
label = per_cell[target][0][0]
path: list[Label] = []
while True:
path.insert(0, label)
if label.parent is None:
break
label = label.parent
path: list[Label] = []
while True:
path.insert(0, label)
if label.parent is None:
break
label = label.parent
p_grid = [[str(c) for c in r] for r in grid]
p_grid = [[str(c) for c in r] for r in grid]
for i in range(len(grid)):
for j in range(len(grid[0])):
if per_cell[i, j]:
p_grid[i][j] = f"\033[94m{grid[i][j]}\033[0m"
for i in range(len(grid)):
for j in range(len(grid[0])):
if per_cell[i, j]:
p_grid[i][j] = f"\033[94m{grid[i][j]}\033[0m"
prev_label = path[0]
for label in path[1:]:
for r in range(
min(prev_label.row, label.row), max(prev_label.row, label.row) + 1
prev_label = path[0]
for label in path[1:]:
for r in range(
min(prev_label.row, label.row), max(prev_label.row, label.row) + 1
):
for c in range(
min(prev_label.col, label.col),
max(prev_label.col, label.col) + 1,
):
for c in range(
min(prev_label.col, label.col),
max(prev_label.col, label.col) + 1,
):
if (r, c) != (prev_label.row, prev_label.col):
p_grid[r][c] = f"\033[93m{grid[r][c]}\033[0m"
if (r, c) != (prev_label.row, prev_label.col):
p_grid[r][c] = f"\033[93m{grid[r][c]}\033[0m"
p_grid[label.row][label.col] = (
f"\033[91m{grid[label.row][label.col]}\033[0m"
)
p_grid[label.row][label.col] = f"\033[91m{grid[label.row][label.col]}\033[0m"
prev_label = label
prev_label = label
p_grid[0][0] = f"\033[92m{grid[0][0]}\033[0m"
p_grid[0][0] = f"\033[92m{grid[0][0]}\033[0m"
for row in p_grid:
self.logger.info("".join(row))
print("\n".join("".join(row) for row in p_grid))
def shortest_many_paths(self, grid: list[list[int]]) -> dict[tuple[int, int], int]:
n_rows, n_cols = len(grid), len(grid[0])
visited: dict[tuple[int, int], tuple[Label, int]] = {}
def shortest_many_paths(grid: list[list[int]]) -> dict[tuple[int, int], int]:
n_rows, n_cols = len(grid), len(grid[0])
queue: list[tuple[int, Label]] = [
(0, Label(row=n_rows - 1, col=n_cols - 1, direction="^", count=0))
]
visited: dict[tuple[int, int], tuple[Label, int]] = {}
while queue and len(visited) != n_rows * n_cols:
distance, label = heapq.heappop(queue)
queue: list[tuple[int, Label]] = [
(0, Label(row=n_rows - 1, col=n_cols - 1, direction="^", count=0))
]
if (label.row, label.col) in visited:
while queue and len(visited) != n_rows * n_cols:
distance, label = heapq.heappop(queue)
if (label.row, label.col) in visited:
continue
visited[label.row, label.col] = (label, distance)
for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
if label.direction == i_direction:
continue
else:
row, col = (label.row + c_row, label.col + c_col)
# exclude labels outside the grid or with too many moves in the same
# direction
if row not in range(0, n_rows) or col not in range(0, n_cols):
continue
visited[label.row, label.col] = (label, distance)
for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
if label.direction == i_direction:
continue
else:
row, col = (label.row + c_row, label.col + c_col)
# exclude labels outside the grid or with too many moves in the same
# direction
if row not in range(0, n_rows) or col not in range(0, n_cols):
continue
heapq.heappush(
queue,
(
distance
+ sum(
grid[r][c]
for r in range(min(row, label.row), max(row, label.row) + 1)
for c in range(min(col, label.col), max(col, label.col) + 1)
)
- grid[row][col],
Label(
row=row,
col=col,
direction=direction,
count=0,
parent=label,
),
),
)
return {(r, c): visited[r, c][1] for r in range(n_rows) for c in range(n_cols)}
def shortest_path(
self,
grid: list[list[int]],
min_straight: int,
max_straight: int,
lower_bounds: dict[tuple[int, int], int],
) -> int:
n_rows, n_cols = len(grid), len(grid[0])
target = (len(grid) - 1, len(grid[0]) - 1)
# for each tuple (row, col, direction, count), the associated label when visited
visited: dict[tuple[int, int, str, int], Label] = {}
# list of all visited labels for a cell (with associated distance)
per_cell: dict[tuple[int, int], list[tuple[Label, int]]] = defaultdict(list)
# need to add two start labels, otherwise one of the two possible direction will
# not be possible
queue: list[tuple[int, int, Label]] = [
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="^", count=0)),
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="<", count=0)),
]
while queue:
_, distance, label = heapq.heappop(queue)
if (label.row, label.col, label.direction, label.count) in visited:
continue
visited[label.row, label.col, label.direction, label.count] = label
per_cell[label.row, label.col].append((label, distance))
if (label.row, label.col) == target:
break
for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
# cannot move in the opposite direction
if label.direction == i_direction:
continue
# other direction, move 'min_straight' in the new direction
elif label.direction != direction:
row, col, count = (
label.row + min_straight * c_row,
label.col + min_straight * c_col,
min_straight,
)
# same direction, too many count
elif label.count == max_straight:
continue
# same direction, keep going and increment count
else:
row, col, count = (
label.row + c_row,
label.col + c_col,
label.count + 1,
)
# exclude labels outside the grid or with too many moves in the same
# direction
if row not in range(0, n_rows) or col not in range(0, n_cols):
continue
distance_to = (
heapq.heappush(
queue,
(
distance
+ sum(
grid[r][c]
for r in range(min(row, label.row), max(row, label.row) + 1)
for c in range(min(col, label.col), max(col, label.col) + 1)
)
- grid[label.row][label.col]
)
heapq.heappush(
queue,
(
distance_to + lower_bounds[row, col],
distance_to,
Label(
row=row,
col=col,
direction=direction,
count=count,
parent=label,
),
- grid[row][col],
Label(
row=row,
col=col,
direction=direction,
count=0,
parent=label,
),
),
)
return {(r, c): visited[r, c][1] for r in range(n_rows) for c in range(n_cols)}
def shortest_path(
grid: list[list[int]],
min_straight: int,
max_straight: int,
lower_bounds: dict[tuple[int, int], int],
) -> int:
n_rows, n_cols = len(grid), len(grid[0])
target = (len(grid) - 1, len(grid[0]) - 1)
# for each tuple (row, col, direction, count), the associated label when visited
visited: dict[tuple[int, int, str, int], Label] = {}
# list of all visited labels for a cell (with associated distance)
per_cell: dict[tuple[int, int], list[tuple[Label, int]]] = defaultdict(list)
# need to add two start labels, otherwise one of the two possible direction will
# not be possible
queue: list[tuple[int, int, Label]] = [
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="^", count=0)),
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="<", count=0)),
]
while queue:
_, distance, label = heapq.heappop(queue)
if (label.row, label.col, label.direction, label.count) in visited:
continue
visited[label.row, label.col, label.direction, label.count] = label
per_cell[label.row, label.col].append((label, distance))
if (label.row, label.col) == target:
break
for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
# cannot move in the opposite direction
if label.direction == i_direction:
continue
# other direction, move 'min_straight' in the new direction
elif label.direction != direction:
row, col, count = (
label.row + min_straight * c_row,
label.col + min_straight * c_col,
min_straight,
)
if self.verbose:
self.print_shortest_path(grid, target, per_cell)
# same direction, too many count
elif label.count == max_straight:
continue
return per_cell[target][0][1]
# same direction, keep going and increment count
else:
row, col, count = (
label.row + c_row,
label.col + c_col,
label.count + 1,
)
# exclude labels outside the grid or with too many moves in the same
# direction
if row not in range(0, n_rows) or col not in range(0, n_cols):
continue
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)
distance_to = (
distance
+ sum(
grid[r][c]
for r in range(min(row, label.row), max(row, label.row) + 1)
for c in range(min(col, label.col), max(col, label.col) + 1)
)
- grid[label.row][label.col]
)
# part 1
yield self.shortest_path(data, 1, 3, lower_bounds=estimates)
heapq.heappush(
queue,
(
distance_to + lower_bounds[row, col],
distance_to,
Label(
row=row,
col=col,
direction=direction,
count=count,
parent=label,
),
),
)
# part 2
yield self.shortest_path(data, 4, 10, lower_bounds=estimates)
if VERBOSE:
print_shortest_path(grid, target, per_cell)
return per_cell[target][0][1]
data = [[int(c) for c in r] for r in sys.stdin.read().splitlines()]
estimates = shortest_many_paths(data)
# part 1
answer_1 = shortest_path(data, 1, 3, lower_bounds=estimates)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = shortest_path(data, 4, 10, lower_bounds=estimates)
print(f"answer 2 is {answer_2}")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,53 +1,53 @@
import string
import sys
from collections import defaultdict
from typing import Any, Iterator
from ..base import BaseSolver
NOT_A_SYMBOL = "." + string.digits
lines = sys.stdin.read().splitlines()
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
values: list[int] = []
gears: dict[tuple[int, int], list[int]] = defaultdict(list)
values: list[int] = []
gears: dict[tuple[int, int], list[int]] = defaultdict(list)
for i, line in enumerate(lines):
j = 0
while j < len(line):
# skip everything until a digit is found (start of a number)
if line[j] not in string.digits:
j += 1
continue
for i, line in enumerate(lines):
j = 0
while j < len(line):
# skip everything until a digit is found (start of a number)
if line[j] not in string.digits:
j += 1
continue
# extract the range of the number and its value
k = j + 1
while k < len(line) and line[k] in string.digits:
k += 1
# extract the range of the number and its value
k = j + 1
while k < len(line) and line[k] in string.digits:
k += 1
value = int(line[j:k])
value = int(line[j:k])
# lookup around the number if there is a symbol - we go through the number
# itself but that should not matter since it only contains digits
found = False
for i2 in range(max(0, i - 1), min(i + 1, len(lines) - 1) + 1):
for j2 in range(max(0, j - 1), min(k, len(line) - 1) + 1):
assert i2 >= 0 and i2 < len(lines)
assert j2 >= 0 and j2 < len(line)
# lookup around the number if there is a symbol - we go through the number
# itself but that should not matter since it only contains digits
found = False
for i2 in range(max(0, i - 1), min(i + 1, len(lines) - 1) + 1):
for j2 in range(max(0, j - 1), min(k, len(line) - 1) + 1):
assert i2 >= 0 and i2 < len(lines)
assert j2 >= 0 and j2 < len(line)
if lines[i2][j2] not in NOT_A_SYMBOL:
found = True
if lines[i2][j2] not in NOT_A_SYMBOL:
found = True
if lines[i2][j2] == "*":
gears[i2, j2].append(value)
if lines[i2][j2] == "*":
gears[i2, j2].append(value)
if found:
values.append(value)
if found:
values.append(value)
# continue starting from the end of the number
j = k
# continue starting from the end of the number
j = k
# part 1
answer_1 = sum(values)
print(f"answer 1 is {answer_1}")
yield sum(values)
yield sum(v1 * v2 for v1, v2 in filter(lambda vs: len(vs) == 2, gears.values()))
# 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,7 +1,5 @@
import sys
from dataclasses import dataclass
from typing import Any, Iterator
from ..base import BaseSolver
@dataclass(frozen=True)
@@ -11,34 +9,33 @@ class Card:
values: list[int]
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
lines = sys.stdin.read().splitlines()
cards: list[Card] = []
for line in lines:
id_part, e_part = line.split(":")
numbers_s, values_s = e_part.split("|")
cards.append(
Card(
id=int(id_part.split()[1]),
numbers=[int(v.strip()) for v in numbers_s.strip().split()],
values=[int(v.strip()) for v in values_s.strip().split()],
)
)
cards: list[Card] = []
for line in lines:
id_part, e_part = line.split(":")
numbers_s, values_s = e_part.split("|")
cards.append(
Card(
id=int(id_part.split()[1]),
numbers=[int(v.strip()) for v in numbers_s.strip().split()],
values=[int(v.strip()) for v in values_s.strip().split()],
)
)
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
yield sum(2 ** (winning - 1) for winning in winnings if winning > 0)
# part 1
answer_1 = sum(2 ** (winning - 1) for winning in winnings if winning > 0)
print(f"answer 1 is {answer_1}")
# part 2
card2cards = {i: list(range(i + 1, i + w + 1)) for i, w in enumerate(winnings)}
card2values = {i: 0 for i in range(len(cards))}
# part 2
card2cards = {i: list(range(i + 1, i + w + 1)) for i, w in enumerate(winnings)}
card2values = {i: 0 for i in range(len(cards))}
for i in range(len(cards)):
card2values[i] += 1
for j in card2cards[i]:
card2values[j] += card2values[i]
for i in range(len(cards)):
card2values[i] += 1
for j in card2cards[i]:
card2values[j] += card2values[i]
yield sum(card2values.values())
print(f"answer 2 is {sum(card2values.values())}")

View File

@@ -1,6 +1,5 @@
from typing import Any, Iterator, Sequence
from ..base import BaseSolver
import sys
from typing import Sequence
MAP_ORDER = [
"seed",
@@ -13,6 +12,55 @@ MAP_ORDER = [
"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(
values: tuple[int, int], map: list[tuple[int, int, int]]
@@ -63,71 +111,19 @@ def find_range(
return ranges
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
def find_location_ranges(seeds: Sequence[tuple[int, int]]) -> Sequence[tuple[int, int]]:
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])]
return seeds
# 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:]):
seeds = [s2 for s1 in seeds for s2 in find_range(s1, maps[map1, map2])]
return seeds
# part 1 - use find_range() with range of length 1
seeds_p1 = [(int(s), 1) for s in lines[0].split(":")[1].strip().split()]
answer_1 = min(start for start, _ in find_location_ranges(seeds_p1))
print(f"answer 1 is {answer_1}")
# 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
# 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))
# # 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])]
answer_2 = min(start for start, _ in find_location_ranges(seeds_p2))
print(f"answer 2 is {answer_2}")

View File

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

View File

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

View File

@@ -1,30 +1,29 @@
import itertools
import math
from typing import Any, Iterator
import sys
from ..base import BaseSolver
lines = sys.stdin.read().splitlines()
sequence = lines[0]
nodes = {
p[0]: {d: n for d, n in zip("LR", p[1].strip("()").split(", "))}
for line in lines[2:]
if (p := line.split(" = "))
}
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
def path(start: str):
path = [start]
it_seq = iter(itertools.cycle(sequence))
while not path[-1].endswith("Z"):
path.append(nodes[path[-1]][next(it_seq)])
return path
sequence = lines[0]
nodes = {
p[0]: {d: n for d, n in zip("LR", p[1].strip("()").split(", "))}
for line in lines[2:]
if (p := line.split(" = "))
}
def path(start: str):
path = [start]
it_seq = iter(itertools.cycle(sequence))
while not path[-1].endswith("Z"):
path.append(nodes[path[-1]][next(it_seq)])
return path
# part 1
answer_1 = len(path(next(node for node in nodes if node.endswith("A")))) - 1
print(f"answer 1 is {answer_1}")
# part 1
yield len(path(next(node for node in nodes if node.endswith("A")))) - 1
# part 2
yield math.lcm(*(len(path(node)) - 1 for node in nodes if node.endswith("A")))
# 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,34 +1,29 @@
from typing import Any, Iterator
import sys
from ..base import BaseSolver
lines = sys.stdin.read().splitlines()
data = [[int(c) for c in line.split()] for line in lines]
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
right_values: list[int] = []
left_values: list[int] = []
for values in data:
diffs = [values]
while any(d != 0 for d in diffs[-1]):
diffs.append([rhs - lhs for lhs, rhs in zip(diffs[-1][:-1], diffs[-1][1:])])
data = [[int(c) for c in line.split()] for line in lines]
rhs: list[int] = [0]
lhs: list[int] = [0]
for cx in range(len(diffs) - 1):
rhs.append(diffs[-cx - 2][-1] + rhs[cx])
lhs.append(diffs[-cx - 2][0] - lhs[cx])
right_values: list[int] = []
left_values: list[int] = []
for values in data:
diffs = [values]
while any(d != 0 for d in diffs[-1]):
diffs.append(
[rhs - lhs for lhs, rhs in zip(diffs[-1][:-1], diffs[-1][1:])]
)
right_values.append(rhs[-1])
left_values.append(lhs[-1])
rhs: list[int] = [0]
lhs: list[int] = [0]
for cx in range(len(diffs) - 1):
rhs.append(diffs[-cx - 2][-1] + rhs[cx])
lhs.append(diffs[-cx - 2][0] - lhs[cx])
# part 1
answer_1 = sum(right_values)
print(f"answer 1 is {answer_1}")
right_values.append(rhs[-1])
left_values.append(lhs[-1])
# part 1
yield sum(right_values)
# part 2
yield sum(left_values)
# part 2
answer_2 = sum(left_values)
print(f"answer 2 is {answer_2}")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,23 +1,22 @@
from typing import Any, Iterator
from ..base import BaseSolver
import sys
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
def is_safe(level: list[int]) -> bool:
diff = [a - b for a, b in zip(level[:-1], level[1:], strict=True)]
def is_safe(level: list[int]) -> bool:
diff = [a - b for a, b in zip(level[:-1], level[1:], strict=True)]
return sum(d > 0 for d in diff) in (0, len(diff)) and all(
1 <= abs(d) <= 3 for d in diff
)
return sum(d > 0 for d in diff) in (0, len(diff)) and all(
1 <= abs(d) <= 3 for d in diff
)
def is_any_safe(level: list[int]) -> bool:
return any(
is_safe(level[:i] + level[i + 1 :]) for i in range(0, len(level))
)
levels = [[int(c) for c in r.split()] for r in input.splitlines()]
def is_any_safe(level: list[int]) -> bool:
return any(is_safe(level[:i] + level[i + 1 :]) for i in range(0, len(level)))
yield sum(is_safe(level) for level in levels)
yield sum(is_safe(level) or is_any_safe(level) for level in levels)
levels = [[int(c) for c in r.split()] for r in sys.stdin.read().strip().splitlines()]
answer_1 = sum(is_safe(level) for level in levels)
answer_2 = sum(is_safe(level) or is_any_safe(level) for level in levels)
print(f"answer 1 is {answer_1}")
print(f"answer 2 is {answer_2}")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,30 +1,34 @@
import re
from typing import Any, Iterator
from ..base import BaseSolver
import sys
from typing import Iterator
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
def extract_multiply(line: str) -> Iterator[int]:
for m in re.finditer(r"mul\(([0-9]{1,3}),\s*([0-9]{1,3})\)", line):
yield int(m.group(1)) * int(m.group(2))
def extract_multiply(line: str) -> Iterator[int]:
for m in re.finditer(r"mul\(([0-9]{1,3}),\s*([0-9]{1,3})\)", line):
yield int(m.group(1)) * int(m.group(2))
def valid_memory_blocks(line: str) -> Iterator[str]:
accumulate = True
while line:
if accumulate:
if (dont_i := line.find("don't()")) != -1:
yield line[:dont_i]
line, accumulate = line[dont_i:], False
else:
yield line
line = ""
else:
if (do_i := line.find("do()")) != -1:
line, accumulate = line[do_i:], True
else:
line = ""
yield sum(extract_multiply(input))
yield sum(sum(extract_multiply(block)) for block in valid_memory_blocks(input))
def valid_memory_blocks(line: str) -> Iterator[str]:
accumulate = True
while line:
if accumulate:
if (dont_i := line.find("don't()")) != -1:
yield line[:dont_i]
line, accumulate = line[dont_i:], False
else:
yield line
line = ""
else:
if (do_i := line.find("do()")) != -1:
line, accumulate = line[do_i:], True
else:
line = ""
line = sys.stdin.read().strip()
answer_1 = sum(extract_multiply(line))
answer_2 = sum(sum(extract_multiply(block)) for block in valid_memory_blocks(line))
print(f"answer 1 is {answer_1}")
print(f"answer 2 is {answer_2}")

View File

@@ -1,37 +1,10 @@
import itertools as it
from typing import Any, Iterator
import sys
from ..base import BaseSolver
lines = sys.stdin.read().splitlines()
answer_1 = ...
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
n = len(lines)
answer_2 = ...
yield sum(
line.count("XMAS") + line.count("SAMX")
for i in range(n)
for ri, rk, ro, ci, ck, cm in (
(1, 0, 0, 0, 1, n),
(0, 1, 0, 1, 0, n),
(0, 1, 0, 1, 1, n - i),
(0, -1, -1, 1, 1, n - i),
(1, 1, 0, 0, 1, n - i if i != 0 else 0),
(-1, -1, -1, 0, 1, n - i if i != 0 else 0),
)
if (
line := "".join(
lines[ri * i + rk * k + ro][ci * i + ck * k] for k in range(cm)
)
)
)
yield sum(
lines[i][j] == "A"
and "".join(
lines[i + di][j + dj] for di, dj in it.product((-1, 1), (-1, 1))
)
in {"MSMS", "SSMM", "MMSS", "SMSM"}
for i, j in it.product(range(1, n - 1), range(1, n - 1))
)
print(f"answer 1 is {answer_1}")
print(f"answer 2 is {answer_2}")

View File

@@ -1,65 +1,10 @@
from collections import defaultdict
from typing import Any, Iterator
import sys
from ..base import BaseSolver
lines = sys.stdin.read().splitlines()
answer_1 = ...
def in_correct_order(update: list[int], requirements: dict[int, set[int]]) -> bool:
return all(
not any(value_2 in requirements[value] for value_2 in update[i_value:])
for i_value, value in enumerate(update)
)
answer_2 = ...
def to_correct_order(
update: list[int],
requirements: dict[int, set[int]],
max_update_length: int | None = None,
) -> list[int]:
# copy requirements to update
requirements = {
value: {predecessor for predecessor in predecessors if predecessor in update}
for value, predecessors in requirements.items()
if value in update
}
max_update_length = max_update_length or len(update)
update = []
while requirements and len(update) < max_update_length:
value = next(
value for value, predecessors in requirements.items() if not predecessors
)
update.append(value)
del requirements[value]
for predecessors in requirements.values():
if value in predecessors:
predecessors.remove(value)
return update
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
part1, part2 = input.split("\n\n")
requirements: dict[int, set[int]] = defaultdict(set)
for line in part1.splitlines():
v1, v2 = line.split("|")
requirements[int(v2)].add(int(v1))
updates = [list(map(int, line.split(","))) for line in part2.splitlines()]
yield sum(
update[len(update) // 2]
for update in updates
if in_correct_order(update, requirements)
)
yield sum(
to_correct_order(update, requirements, len(update) // 2 + 1)[-1]
for update in updates
if not in_correct_order(update, requirements)
)
print(f"answer 1 is {answer_1}")
print(f"answer 2 is {answer_2}")

View File

@@ -1,122 +1,10 @@
import itertools as it
from typing import Any, Iterator, TypeAlias
import sys
from ..base import BaseSolver
lines = sys.stdin.read().splitlines()
NodeType: TypeAlias = tuple[tuple[int, int], tuple[int, int]]
EdgesType: TypeAlias = dict[NodeType, tuple[NodeType, set[tuple[int, int]]]]
answer_1 = ...
ROTATE = {(-1, 0): (0, 1), (0, 1): (1, 0), (1, 0): (0, -1), (0, -1): (-1, 0)}
answer_2 = ...
START_NODE: NodeType = ((-2, -2), (-1, 0))
FINAL_POS: tuple[int, int] = (-1, -1)
def move(
lines: list[str], pos: tuple[int, int], dir: tuple[int, int]
) -> tuple[tuple[int, int] | None, set[tuple[int, int]]]:
n_rows, n_cols = len(lines), len(lines[0])
row, col = pos
marked: set[tuple[int, int]] = set()
final_pos: tuple[int, int] | None = None
while True:
marked.add((row, col))
if not (0 <= row + dir[0] < n_rows and 0 <= col + dir[1] < n_cols):
final_pos = None
break
if lines[row + dir[0]][col + dir[1]] != ".":
final_pos = (row, col)
break
row += dir[0]
col += dir[1]
return final_pos, marked
def compute_graph(lines: list[str], start_node: NodeType):
n_rows, n_cols = len(lines), len(lines[0])
edges: EdgesType = {}
start_pos, start_dir = start_node
end_pos, marked = move(lines, start_pos, start_dir)
assert end_pos is not None
edges[START_NODE] = ((end_pos, start_dir), marked)
for row, col in it.product(range(n_rows), range(n_cols)):
if lines[row][col] != "#":
continue
for start_pos, start_dir in (
((row - 1, col), (1, 0)),
((row + 1, col), (-1, 0)),
((row, col - 1), (0, 1)),
((row, col + 1), (0, -1)),
):
if 0 <= start_pos[0] < n_rows and 0 <= start_pos[1] < n_cols:
end_pos, marked = move(lines, start_pos, ROTATE[start_dir])
edges[start_pos, start_dir] = (
(end_pos or FINAL_POS, ROTATE[start_dir]),
marked,
)
return edges
def is_loop(lines: list[str], edges: EdgesType, position: tuple[int, int]):
row, col = position
current_node = START_NODE
found: set[NodeType] = set()
while current_node[0] != FINAL_POS and current_node not in found:
found.add(current_node)
target_node, edge_marked = edges[current_node]
if (row, col) in edge_marked:
# need to break the edge
target_dir = target_node[1]
end_pos, _ = move(
lines, (row - target_dir[0], col - target_dir[1]), ROTATE[target_dir]
)
current_node = (end_pos or FINAL_POS, ROTATE[target_dir])
else:
current_node = target_node
return current_node in found
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
# read lines
lines = input.splitlines()
# find and delete original position
start_pos = next(
(i, j)
for i, row in enumerate(lines)
for j, col in enumerate(row)
if col == "^"
)
lines[start_pos[0]] = lines[start_pos[0]].replace("^", ".")
# compute edges from the map
edges = compute_graph(lines, (start_pos, (-1, 0)))
# part 1
marked: set[tuple[int, int]] = set()
current_node = START_NODE
while current_node[0] != FINAL_POS:
current_node, current_marked = edges[current_node]
marked = marked.union(current_marked)
yield len(marked)
yield sum(is_loop(lines, edges, pos) for pos in marked if pos != start_pos)
print(f"answer 1 is {answer_1}")
print(f"answer 2 is {answer_2}")

View File

@@ -1,50 +1,10 @@
from typing import Any, Iterator
import sys
from ..base import BaseSolver
lines = sys.stdin.read().splitlines()
answer_1 = ...
def evaluate(
target: int, numbers: list[int], concatenate: bool = False, current: int = 0
) -> bool:
if not numbers:
return current == target
answer_2 = ...
if current > target:
return False
head, *tail = numbers
if evaluate(target, tail, concatenate, current + head) or evaluate(
target, tail, concatenate, current * head
):
return True
if not concatenate:
return False
return evaluate(target, tail, concatenate, int(str(current) + str(head)))
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
targets = {
int(part[0]): list(map(int, part[1].strip().split()))
for line in input.splitlines()
if (part := line.split(":"))
}
yield sum(
target
for target, numbers in self.progress.wrap(
targets.items(), total=len(targets)
)
if evaluate(target, numbers)
)
yield sum(
target
for target, numbers in self.progress.wrap(
targets.items(), total=len(targets)
)
if evaluate(target, numbers, True)
)
print(f"answer 1 is {answer_1}")
print(f"answer 2 is {answer_2}")

View File

@@ -1,76 +1,10 @@
import itertools as it
from collections import defaultdict
from typing import Any, Iterator, cast
import sys
from ..base import BaseSolver
lines = sys.stdin.read().splitlines()
answer_1 = ...
def compute_antinodes(
a1: tuple[int, int],
a2: tuple[int, int],
n_rows: int,
n_cols: int,
min_distance: int = 1,
max_distance: int | None = 1,
):
if a1[0] > a2[0]:
a1, a2 = a2, a1
answer_2 = ...
d_row, d_col = a2[0] - a1[0], a2[1] - a1[1]
points: list[tuple[int, int]] = []
for c in range(min_distance, (max_distance or n_rows) + 1):
row_1, col_1 = a1[0] - c * d_row, a1[1] - c * d_col
row_2, col_2 = a2[0] + c * d_row, a2[1] + c * d_col
valid_1, valid_2 = (
0 <= row_1 < n_rows and 0 <= col_1 < n_cols,
0 <= row_2 < n_rows and 0 <= col_2 < n_cols,
)
if not valid_1 and not valid_2:
break
if valid_1:
points.append((row_1, col_1))
if valid_2:
points.append((row_2, col_2))
return tuple(points)
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
n_rows, n_cols = len(lines), len(lines[0])
antennas: dict[str, list[tuple[int, int]]] = defaultdict(list)
for i, j in it.product(range(n_rows), range(n_cols)):
if lines[i][j] != ".":
antennas[lines[i][j]].append((i, j))
yield len(
cast(set[tuple[int, int]], set()).union(
it.chain(
*(
compute_antinodes(a1, a2, n_rows, n_cols)
for antennas_of_frequency in antennas.values()
for a1, a2 in it.permutations(antennas_of_frequency, 2)
)
)
)
)
yield len(
cast(set[tuple[int, int]], set()).union(
it.chain(
*(
compute_antinodes(a1, a2, n_rows, n_cols, 0, None)
for antennas_of_frequency in antennas.values()
for a1, a2 in it.permutations(antennas_of_frequency, 2)
)
)
)
)
print(f"answer 1 is {answer_1}")
print(f"answer 2 is {answer_2}")

View File

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

View File

@@ -1,114 +1,14 @@
import argparse
import importlib
import json
import logging
import logging.handlers
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Iterable, Iterator, Literal, Sequence, TextIO, TypeVar
from tqdm import tqdm
from .base import BaseSolver
_T = TypeVar("_T")
def dump_api_message(
type: Literal["log", "answer", "progress-start", "progress-step", "progress-end"],
content: Any,
file: TextIO = sys.stdout,
):
print(
json.dumps(
{"type": type, "time": datetime.now().isoformat(), "content": content}
),
flush=True,
file=file,
)
class LoggerAPIHandler(logging.Handler):
def __init__(self, output: TextIO = sys.stdout):
super().__init__()
self.output = output
def emit(self, record: logging.LogRecord):
dump_api_message(
"log", {"level": record.levelname, "message": record.getMessage()}
)
class ProgressAPI:
def __init__(
self,
min_step: int = 1,
min_time: timedelta = timedelta(milliseconds=100),
output: TextIO = sys.stdout,
):
super().__init__()
self.counter = 0
self.output = output
self.min_step = min_step
self.min_time = min_time
def wrap(
self, values: Sequence[_T] | Iterable[_T], total: int | None = None
) -> Iterator[_T]:
total = total or len(values) # type: ignore
current = self.counter
self.counter += 1
dump_api_message("progress-start", {"counter": current, "total": total})
try:
percent = 0
time = datetime.now()
for i_value, value in enumerate(values):
yield value
if datetime.now() - time < self.min_time:
continue
time = datetime.now()
c_percent = round(i_value / total * 100)
if c_percent >= percent + self.min_step:
dump_api_message(
"progress-step", {"counter": current, "percent": c_percent}
)
percent = c_percent
finally:
dump_api_message(
"progress-end",
{"counter": current},
)
class ProgressTQDM:
def wrap(
self, values: Sequence[_T] | Iterable[_T], total: int | None = None
) -> Iterator[_T]:
return iter(tqdm(values, total=total))
class ProgressNone:
def wrap(
self, values: Sequence[_T] | Iterable[_T], total: int | None = None
) -> Iterator[_T]:
return iter(values)
def main():
parser = argparse.ArgumentParser("Holt59 Advent-Of-Code Runner")
parser.add_argument("-v", "--verbose", action="store_true", help="verbose mode")
parser.add_argument("-t", "--test", action="store_true", help="test mode")
parser.add_argument("-a", "--api", action="store_true", help="API mode")
parser.add_argument(
"-u", "--user", type=str, default="holt59", help="user input to use"
)
@@ -131,7 +31,6 @@ def main():
args = parser.parse_args()
verbose: bool = args.verbose
api: bool = args.api
test: bool = args.test
stdin: bool = args.stdin
user: str = args.user
@@ -141,10 +40,8 @@ def main():
day: int = args.day
# TODO: change this
logging.basicConfig(
level=logging.INFO if verbose or api else logging.WARNING,
handlers=[LoggerAPIHandler()] if api else None,
)
if verbose:
os.environ["AOC_VERBOSE"] = "True"
if input_path is None:
input_path = Path(__file__).parent.joinpath(
@@ -152,55 +49,11 @@ def main():
)
assert input_path.exists(), f"{input_path} missing"
solver_class: type[BaseSolver] = importlib.import_module(
f".{year}.day{day}", __package__
).Solver
solver = solver_class(
logging.getLogger("AOC"),
verbose=verbose,
year=year,
day=day,
progress=ProgressAPI()
if api
else ProgressTQDM()
if verbose
else ProgressNone(), # type: ignore
outputs=not api,
)
data: str
if stdin:
data = sys.stdin.read()
importlib.import_module(f".{year}.day{day}", __package__)
else:
with open(input_path) as fp:
data = fp.read()
sys.stdin = fp
importlib.import_module(f".{year}.day{day}", __package__)
start = datetime.now()
last = start
it = solver.solve(data.strip())
if it is None:
solver.logger.error(f"no implementation for {year} day {day}")
exit()
for i_answer, answer in enumerate(it):
current = datetime.now()
if api:
dump_api_message(
"answer",
{
"answer": i_answer + 1,
"value": answer,
"answerTime_s": (current - last).total_seconds(),
"totalTime_s": (current - start).total_seconds(),
},
)
else:
print(
f"answer {i_answer + 1} is {answer} (found in {(current - last).total_seconds():.2f}s)"
)
last = current
sys.stdin = sys.__stdin__

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