75 Commits

Author SHA1 Message Date
Mikaël Capelle
c69694da7e Force string type for answer value.
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-08 14:34:04 +01:00
Mikaël Capelle
d7c5b1b658 Apply formatting.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-12-08 14:05:14 +01:00
Mikaël Capelle
1b4dd32898 Refactor 2021 for new UI.
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2024-12-08 14:03:34 +01:00
Mikaël Capelle
cd96140378 Start fixing 2022 for new API. 2024-12-08 13:55:25 +01:00
Mikaël Capelle
377e501d34 Start fixing 2015 for new API.
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2024-12-08 12:13:41 +01:00
Mikaël Capelle
d1f4f5bed0 2024 day 8.
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2024-12-08 08:53:36 +01:00
Mikaël Capelle
03a950c485 Force flush API message.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-07 12:38:02 +01:00
Mikaël Capelle
22b1513271 Fix answer API model.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-07 12:21:57 +01:00
Mikaël Capelle
1f5b21a13a 2024 day 7.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-07 09:39:17 +01:00
Mikaël Capelle
8c707c00ba 2024 day 6 new API.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-06 21:15:18 +01:00
Mikael CAPELLE
ae4f42517c Start adding progress.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-06 14:39:23 +01:00
Mikael CAPELLE
17432f7ac6 Refactor main entrypoint for UI. 2024-12-06 14:11:35 +01:00
Mikael CAPELLE
664dcfe7ba Refactor 2023 for new system. 2024-12-06 14:11:35 +01:00
Mikael CAPELLE
a9bcf9ef8f Start refactoring code better flexibility. 2024-12-06 14:11:32 +01:00
Mikael CAPELLE
1caf93b38b Refactor 2024 day 6 to be a little bit faster.
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-06 14:10:51 +01:00
Mikaël Capelle
f9a3dee20b 2024 day 6, brute force.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-06 06:50:35 +01:00
Mikael CAPELLE
1a1ff0c64d Refactor 2024 day 5 without networkx.
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-05 08:28:55 +01:00
Mikaël Capelle
d7621d09b5 2024 day 5.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-05 07:25:55 +01:00
Mikael CAPELLE
b89d29e880 Refactor 2024 day 4.
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-04 09:31:47 +01:00
Mikaël Capelle
f1cd7e6c85 2024 day 4.
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-04 07:35:22 +01:00
Mikael CAPELLE
d16ee7f9ad Refactor 2024 day 3.
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-03 15:22:54 +01:00
0c46d3ed18 Add .drone.yml for CI. (#2)
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-03 13:38:03 +00:00
Mikael CAPELLE
acb767184e Fix linting. 2024-12-03 14:11:29 +01:00
Mikael CAPELLE
cb0145baa2 2024 day 3. 2024-12-03 08:29:25 +01:00
Mikaël Capelle
a4ad0259a9 Fix 2024 day 2. 2024-12-02 18:44:50 +01:00
Mikael CAPELLE
82452c0751 Update Python dependencies. 2024-12-02 17:08:50 +01:00
Mikael CAPELLE
79cc208875 2024 day 2. 2024-12-02 15:42:56 +01:00
Mikaël Capelle
4dd2d5d9c9 2024 day 1. 2024-12-01 10:26:02 +01:00
Mikaël Capelle
def4305c1c 2015 day 22, part 1. 2024-12-01 10:25:49 +01:00
Mikaël Capelle
3edaa249fc 2015 day 21. 2024-01-20 17:57:37 +01:00
Mikaël Capelle
57fcb47fe9 2015 day 20. 2024-01-06 22:07:34 +01:00
Mikaël Capelle
cfa7718475 2015 day 19. 2024-01-06 21:35:48 +01:00
Mikaël Capelle
2d23e355b2 2015 day 18. 2024-01-06 16:43:35 +01:00
Mikaël Capelle
fab4899715 2015 day 17. 2024-01-06 15:46:43 +01:00
Mikaël Capelle
b6e20eefa3 2015 day 16. 2024-01-06 15:27:45 +01:00
Mikaël Capelle
872fd72dcd 2015 day 15. 2024-01-06 15:11:47 +01:00
Mikaël Capelle
98f28e96f8 2015 day 12, 13 & 14. 2024-01-06 14:56:30 +01:00
Mikaël Capelle
ed7aba80ad 2015 day 11. 2024-01-06 11:46:59 +01:00
Mikaël Capelle
e507dad5e0 2015 day 10. 2024-01-06 11:30:10 +01:00
Mikael CAPELLE
04172beb5a 2015 day 9. 2024-01-05 14:46:05 +01:00
Mikael CAPELLE
15ef67e757 2015 day 8. 2024-01-05 10:01:02 +01:00
Mikaël Capelle
cd0ada785c 2015 day 4, 5, 6, 7. 2024-01-04 21:05:42 +01:00
Mikael CAPELLE
42bd8d6983 2015 day 3. 2024-01-04 18:36:30 +01:00
Mikael CAPELLE
0567ab7440 2015 day 1 & 2. 2024-01-04 18:27:17 +01:00
Mikaël Capelle
7d2eb6b5ec Faster 2023 day 24 (part 1). 2024-01-01 18:44:13 +01:00
Mikaël Capelle
3a9c7e728b Minor cleaning 2023. 2023-12-30 19:35:06 +01:00
Mikaël Capelle
d002e419c3 2023 day 25. 2023-12-25 10:36:36 +01:00
Mikaël Capelle
19d93e0c1d 2023 day 24. 2023-12-25 10:36:29 +01:00
Mikaël Capelle
5c05ee5c85 2023 day 23. 2023-12-23 11:05:35 +01:00
Mikaël Capelle
103af21915 2021 day 9. 2023-12-22 21:26:13 +01:00
Mikaël Capelle
af2fbf2da1 2023 day 22. 2023-12-22 14:49:31 +01:00
Mikaël Capelle
c496ea25c9 2023 day 21, version 2. 2023-12-21 21:56:38 +01:00
Mikael CAPELLE
5f8c74fd1c 2023 day 21. 2023-12-21 16:47:43 +01:00
Mikael CAPELLE
dda2be2505 2023 day 20. 2023-12-20 14:27:25 +01:00
Mikael CAPELLE
12891194bb Poetry stuff. 2023-12-19 17:45:33 +01:00
Mikaël Capelle
f15908876d 2023 day 19. 2023-12-19 10:41:53 +01:00
Mikael CAPELLE
5f5ebda674 2023 day 18. 2023-12-18 11:40:32 +01:00
Mikaël Capelle
5b30cc00d5 2023 day 17. 2023-12-17 18:19:49 +01:00
Mikaël Capelle
3a7f8e83dc 2023 day 16. 2023-12-16 18:33:11 +01:00
Mikaël Capelle
ba5b01c594 2023 day 15. 2023-12-15 16:18:21 +01:00
Mikaël Capelle
d0970c090b 2023 day 14. 2023-12-14 19:34:38 +01:00
Mikaël Capelle
8e90bf7002 2023 day 13. 2023-12-13 20:05:05 +01:00
9698dfcdac 2023 day 12. 2023-12-12 20:20:26 +00:00
Mikaël Capelle
1a6ab1cc0e 2023 day 11. 2023-12-11 10:25:56 +01:00
Mikaël Capelle
f5aabbee8f 2021 day 8. 2023-12-10 20:23:22 +01:00
Mikaël Capelle
6c00341ab0 2023 day 10. 2023-12-10 10:09:12 +01:00
Mikaël Capelle
755e0bd4b3 2021 day 7. 2023-12-09 12:52:46 +01:00
Mikaël Capelle
a52d077a40 2021 day 6. 2023-12-09 12:36:10 +01:00
Mikaël Capelle
3fc0f94b1c 2021 day 1-5. 2023-12-09 11:01:28 +01:00
Mikaël Capelle
8a0412f926 Clean 2022. 2023-12-09 09:54:53 +01:00
Mikaël Capelle
855efeb0aa 2023 day 9. 2023-12-09 08:08:46 +01:00
Mikaël Capelle
f2a65e03e5 2023 day 8. 2023-12-08 08:44:03 +01:00
Mikaël Capelle
759f47bfab 2023 day 7. 2023-12-08 08:38:08 +01:00
Mikael CAPELLE
999207b007 Clean 2023. 2023-12-06 08:47:59 +01:00
Mikaël Capelle
d92e4744a4 2023 day 6. 2023-12-06 07:14:26 +01:00
434 changed files with 31382 additions and 1905 deletions

12
.drone.yml Normal file
View File

@@ -0,0 +1,12 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: tests
image: python:3.10-slim
commands:
- pip install poetry
- poetry install
- poetry run poe lint

5
.gitignore vendored
View File

@@ -1 +1,6 @@
# python / VS Code
venv venv
__pycache__
.ruff_cache
.vscode
build

View File

@@ -1,52 +0,0 @@
# -*- encoding: utf-8 -*-
import sys
from collections import defaultdict
import numpy as np
lines: list[str] = sys.stdin.read().splitlines()
sections: list[tuple[tuple[int, int], tuple[int, int]]] = [
(
(
int(line.split(" -> ")[0].split(",")[0]),
int(line.split(" -> ")[0].split(",")[1]),
),
(
int(line.split(" -> ")[1].split(",")[0]),
int(line.split(" -> ")[1].split(",")[1]),
),
)
for line in lines
]
np_sections = np.array(sections).reshape(-1, 4)
x_min, x_max, y_min, y_max = (
min(np_sections[:, 0].min(), np_sections[:, 2].min()),
max(np_sections[:, 0].max(), np_sections[:, 2].max()),
min(np_sections[:, 1].min(), np_sections[:, 3].min()),
max(np_sections[:, 1].max(), np_sections[:, 3].max()),
)
counts_1 = np.zeros((y_max + 1, x_max + 1), dtype=int)
counts_2 = counts_1.copy()
for (x1, y1), (x2, y2) in sections:
x_rng = range(x1, x2 + 1, 1) if x2 >= x1 else range(x1, x2 - 1, -1)
y_rng = range(y1, y2 + 1, 1) if y2 >= y1 else range(y1, y2 - 1, -1)
if x1 == x2 or y1 == y2:
counts_1[list(y_rng), list(x_rng)] += 1
counts_2[list(y_rng), list(x_rng)] += 1
elif abs(x2 - x1) == abs(y2 - y1):
for i, j in zip(y_rng, x_rng):
counts_2[i, j] += 1
answer_1 = (counts_1 >= 2).sum()
print(f"answer 1 is {answer_1}")
answer_2 = (counts_2 >= 2).sum()
print(f"answer 2 is {answer_2}")

View File

@@ -1,9 +0,0 @@
# -*- encoding: utf-8 -*-
import sys
blocks = sys.stdin.read().split("\n\n")
values = sorted(sum(map(int, block.split())) for block in blocks)
print(f"answer 1 is {values[-1]}")
print(f"answer 2 is {sum(values[-3:])}")

View File

@@ -1,40 +0,0 @@
# -*- encoding: utf-8 -*-
import sys
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}")
for i in range(6):
for j in range(40):
v = values[1 + i * 40 + j]
if j >= v - 1 and j <= v + 1:
print("#", end="")
else:
print(".", end="")
print()

View File

@@ -1,41 +0,0 @@
# -*- encoding: utf-8 -*-
import json
import sys
from functools import cmp_to_key
blocks = sys.stdin.read().strip().split("\n\n")
pairs = [tuple(json.loads(p) for p in block.split("\n")) for block in blocks]
def compare(lhs: list[int | list], rhs: list[int | list]) -> int:
for lhs_a, rhs_a in zip(lhs, rhs):
if isinstance(lhs_a, int) and isinstance(rhs_a, int):
if lhs_a != rhs_a:
return rhs_a - lhs_a
else:
if not isinstance(lhs_a, list):
lhs_a = [lhs_a]
elif not isinstance(rhs_a, list):
rhs_a = [rhs_a]
assert isinstance(rhs_a, list) and isinstance(lhs_a, list)
r = compare(lhs_a, rhs_a)
if r != 0:
return r
return len(rhs) - len(lhs)
answer_1 = sum(i + 1 for i, (lhs, rhs) in enumerate(pairs) if compare(lhs, rhs) > 0)
print(f"answer_1 is {answer_1}")
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))))
d_index = [packets.index(d) + 1 for d in dividers]
print(f"answer 2 is {d_index[0] * d_index[1]}")

View File

@@ -1,144 +0,0 @@
# -*- encoding: utf-8 -*-
import sys
from collections import defaultdict
from enum import Enum, auto
from typing import Callable, cast
class Cell(Enum):
AIR = auto()
ROCK = auto()
SAND = auto()
def __str__(self) -> str:
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, y in blocks),
0,
max(x for x, y in blocks),
max(y for x, 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],
fill_fn: Callable[[int, int], Cell],
) -> dict[tuple[int, int], Cell]:
"""
Flow sands onto the given set of blocks
Args:
blocks: Blocks containing ROCK position. Modified in-place.
stop_fn: Function called with the last (assumed) position of a grain of
sand BEFORE adding it to blocks. If the function returns True, the grain
is added and a new one is flowed, otherwise, the whole procedure stops
and the function returns (without adding the final grain).
fill_fn: Function called when the target position of a grain (during the
flowing process) is missing from blocks.
Returns:
The input blocks.
"""
y_max = max(y for x, y in blocks)
while True:
x, y = 500, 0
while y <= y_max:
moved = False
for cx, cy in ((x, y + 1), (x - 1, y + 1), (x + 1, y + 1)):
if (cx, cy) not in blocks and fill_fn(cx, cy) == Cell.AIR:
x, y = cx, cy
moved = True
elif blocks[cx, cy] == Cell.AIR:
x, y = cx, cy
moved = True
if moved:
break
if not moved:
break
if stop_fn(x, y):
break
blocks[x, y] = Cell.SAND
return blocks
# === inputs ===
lines = sys.stdin.read().splitlines()
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
]
)
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
for x in range(x_start, x_end):
for y in range(y_start, y_end):
blocks[x, y] = Cell.ROCK
print_blocks(blocks)
print()
x_min, y_min, x_max, y_max = (
min(x for x, y in blocks),
0,
max(x for x, y in blocks),
max(y for x, y in blocks),
)
# === part 1 ===
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()
# === 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
print_blocks(blocks_2)
print(f"answer 2 is {sum(v == Cell.SAND for v in blocks_2.values())}")

View File

@@ -1,90 +0,0 @@
# -*- encoding: utf-8 -*-
import sys
import numpy as np
import parse
def part1(sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], row: int) -> int:
no_beacons_row_l: list[np.ndarray] = []
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))
no_beacons_row_l.append(sx + np.arange(0, d - abs(sy - row) + 1))
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)
return len(no_beacons_row)
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)
dx = d - abs(sy - y)
if dx >= 0:
its.append((max(0, sx - dx), min(sx + dx, xy_max)))
its = sorted(its)
s, 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(
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"])
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}")
m.set_objective("min", x + y)
s = m.solve()
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 = parse.parse(
"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

@@ -1,53 +0,0 @@
# -*- encoding: utf-8 -*-
import sys
from typing import FrozenSet
import numpy as np
xyz = np.asarray(
[
tuple(int(x) for x in row.split(",")) # type: ignore
for row in sys.stdin.read().splitlines()
]
)
xyz = xyz - xyz.min(axis=0) + 1
cubes = np.zeros(xyz.max(axis=0) + 3, dtype=bool)
cubes[xyz[:, 0], xyz[:, 1], xyz[:, 2]] = True
n_dims = len(cubes.shape)
faces = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)]
answer_1 = sum(
1 for x, y, z in xyz for dx, dy, dz in faces if not cubes[x + dx, y + dy, z + dz]
)
print(f"answer 1 is {answer_1}")
visited = np.zeros_like(cubes, dtype=bool)
queue = [(0, 0, 0)]
n_faces = 0
while queue:
x, y, z = queue.pop(0)
if visited[x, y, z]:
continue
visited[x, y, z] = True
for dx, dy, dz in faces:
nx, ny, nz = x + dx, y + dy, z + dz
if not all(n >= 0 and n < cubes.shape[i] for i, n in enumerate((nx, ny, nz))):
continue
if visited[nx, ny, nz]:
continue
if cubes[nx, ny, nz]:
n_faces += 1
else:
queue.append((nx, ny, nz))
print(f"answer 2 is {n_faces}")

View File

@@ -1,226 +0,0 @@
# -*- encoding: utf-8 -*-
import re
import sys
from typing import Callable
import numpy as np
VOID, EMPTY, WALL = 0, 1, 2
TILE_FROM_CHAR = {" ": VOID, ".": EMPTY, "#": WALL}
SCORES = {"E": 0, "S": 1, "W": 2, "N": 3}
board_map_s, direction_s = sys.stdin.read().split("\n\n")
# board
board_lines = board_map_s.splitlines()
max_line = max(len(line) for line in board_lines)
board = np.array(
[
[TILE_FROM_CHAR[c] for c in row] + [VOID] * (max_line - len(row))
for row in board_map_s.splitlines()
]
)
directions = [
int(p1) if p2 else p1 for p1, p2 in re.findall(R"(([0-9])+|L|R)", direction_s)
]
# find on each row and column the first and last non-void
row_first_non_void = np.argmax(board != VOID, axis=1)
row_last_non_void = board.shape[1] - np.argmax(board[:, ::-1] != VOID, axis=1) - 1
col_first_non_void = np.argmax(board != VOID, axis=0)
col_last_non_void = board.shape[0] - np.argmax(board[::-1, :] != VOID, axis=0) - 1
faces = np.zeros_like(board)
size = np.gcd(board.shape[0], board.shape[1])
for row in range(0, board.shape[0], size):
for col in range(row_first_non_void[row], row_last_non_void[row], size):
faces[row : row + size, col : col + size] = faces.max() + 1
SIZE = np.gcd(*board.shape)
# TODO: deduce this from the actual cube...
faces_wrap: dict[int, dict[str, Callable[[int, int], tuple[int, int, str]]]]
if board.shape == (12, 16): # example
faces_wrap = {
1: {
"W": lambda y, x: (4, 4 + y, "S"), # 3N
"N": lambda y, x: (4, 11 - x, "S"), # 2N
"E": lambda y, x: (11 - y, 15, "W"), # 6E
},
2: {
"W": lambda y, x: (11, 19 - y, "N"), # 6S
"N": lambda y, x: (0, 11 - y, "S"), # 1N
"S": lambda y, x: (11, 11 - x, "N"), # 5S
},
3: {
"N": lambda y, x: (x - 4, 8, "E"), # 1W
"S": lambda y, x: (15 - x, 8, "E"), # 5W
},
4: {"E": lambda y, x: (8, 19 - y, "S")}, # 6N
5: {
"W": lambda y, x: (7, 15 - y, "N"), # 3S
"S": lambda y, x: (7, 11 - x, "N"), # 2S
},
6: {
"N": lambda y, x: (19 - x, 11, "W"), # 4E
"E": lambda y, x: (11 - y, 11, "W"), # 1E
"S": lambda y, x: (19 - x, 0, "E"), # 2W
},
}
else:
faces_wrap = {
1: {
"W": lambda y, x: (3 * SIZE - y - 1, 0, "E"), # 4W
"N": lambda y, x: (2 * SIZE + x, 0, "E"), # 6W
},
2: {
"N": lambda y, x: (4 * SIZE - 1, x - 2 * SIZE, "N"), # 6S
"E": lambda y, x: (3 * SIZE - y - 1, 2 * SIZE - 1, "W"), # 5E
"S": lambda y, x: (x - SIZE, 2 * SIZE - 1, "W"), # 3E
},
3: {
"W": lambda y, x: (2 * SIZE, y - SIZE, "S"), # 4N
"E": lambda y, x: (SIZE - 1, SIZE + y, "N"), # 2S
},
4: {
"W": lambda y, x: (3 * SIZE - y - 1, SIZE, "E"), # 1W
"N": lambda y, x: (SIZE + x, SIZE, "E"), # 3W
},
5: {
"E": lambda y, x: (3 * SIZE - y - 1, 3 * SIZE - 1, "W"), # 2E
"S": lambda y, x: (2 * SIZE + x, SIZE - 1, "W"), # 6E
},
6: {
"W": lambda y, x: (0, y - 2 * SIZE, "S"), # 1N
"E": lambda y, x: (3 * SIZE - 1, y - 2 * SIZE, "N"), # 5S
"S": lambda y, x: (0, x + 2 * SIZE, "S"), # 2N
},
}
def wrap_part_1(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
if r0 == "E":
return y0, row_first_non_void[y0], r0
elif r0 == "S":
return col_first_non_void[x0], x0, r0
elif r0 == "W":
return y0, row_last_non_void[y0], r0
elif r0 == "N":
return col_last_non_void[x0], x0, r0
assert False
def wrap_part_2(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
cube = faces[y0, x0]
assert r0 in faces_wrap[cube]
return faces_wrap[cube][r0](y0, x0)
def run(wrap: Callable[[int, int, str], tuple[int, int, str]]) -> tuple[int, int, str]:
y0 = 0
x0 = np.where(board[0] == EMPTY)[0][0]
r0 = "E"
for direction in directions:
if isinstance(direction, int):
while direction > 0:
if r0 == "E":
xi = np.where(board[y0, x0 + 1 : x0 + direction + 1] == WALL)[0]
if len(xi):
x0 = x0 + xi[0]
direction = 0
elif (
x0 + direction < board.shape[1]
and board[y0, x0 + direction] == EMPTY
):
x0 = x0 + direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
x0 = row_last_non_void[y0]
direction = 0
else:
direction = direction - (row_last_non_void[y0] - x0) - 1
y0, x0, r0 = y0_t, x0_t, r0_t
elif r0 == "S":
yi = np.where(board[y0 + 1 : y0 + direction + 1, x0] == WALL)[0]
if len(yi):
y0 = y0 + yi[0]
direction = 0
elif (
y0 + direction < board.shape[0]
and board[y0 + direction, x0] == EMPTY
):
y0 = y0 + direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
y0 = col_last_non_void[x0]
direction = 0
else:
direction = direction - (col_last_non_void[x0] - y0) - 1
y0, x0, r0 = y0_t, x0_t, r0_t
elif r0 == "W":
left = max(x0 - direction - 1, 0)
xi = np.where(board[y0, left:x0] == WALL)[0]
if len(xi):
x0 = left + xi[-1] + 1
direction = 0
elif x0 - direction >= 0 and board[y0, x0 - direction] == EMPTY:
x0 = x0 - direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
x0 = row_first_non_void[y0]
direction = 0
else:
direction = direction - (x0 - row_first_non_void[y0]) - 1
y0, x0, r0 = y0_t, x0_t, r0_t
elif r0 == "N":
top = max(y0 - direction - 1, 0)
yi = np.where(board[top:y0, x0] == WALL)[0]
if len(yi):
y0 = top + yi[-1] + 1
direction = 0
elif y0 - direction >= 0 and board[y0 - direction, x0] == EMPTY:
y0 = y0 - direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
y0 = col_first_non_void[x0]
direction = 0
else:
direction = direction - (y0 - col_first_non_void[x0]) - 1
y0, x0, r0 = y0_t, x0_t, r0_t
else:
r0 = {
"E": {"L": "N", "R": "S"},
"N": {"L": "W", "R": "E"},
"W": {"L": "S", "R": "N"},
"S": {"L": "E", "R": "W"},
}[r0][direction]
return y0, x0, r0
y1, x1, r1 = run(wrap_part_1)
answer_1 = 1000 * (1 + y1) + 4 * (1 + x1) + SCORES[r1]
print(f"answer 1 is {answer_1}")
y2, x2, r2 = run(wrap_part_2)
answer_2 = 1000 * (1 + y2) + 4 * (1 + x2) + SCORES[r2]
print(f"answer 2 is {answer_2}")

View File

@@ -1,100 +0,0 @@
# -*- encoding: utf-8 -*-
import heapq
import math
import sys
from collections import defaultdict
lines = sys.stdin.read().splitlines()
winds = {
(i - 1, j - 1, lines[i][j])
for i in range(1, len(lines) - 1)
for j in range(1, len(lines[i]) - 1)
if lines[i][j] != "."
}
n_rows, n_cols = len(lines) - 2, len(lines[0]) - 2
CYCLE = math.lcm(n_rows, n_cols)
east_winds = [{j for j in range(n_cols) if (i, j, ">") in winds} for i in range(n_rows)]
west_winds = [{j for j in range(n_cols) if (i, j, "<") in winds} for i in range(n_rows)]
north_winds = [
{i for i in range(n_rows) if (i, j, "^") in winds} for j in range(n_cols)
]
south_winds = [
{i for i in range(n_rows) if (i, j, "v") in winds} for j in range(n_cols)
]
def run(start: tuple[int, int], start_cycle: int, end: tuple[int, int]):
def heuristic(y: int, x: int) -> int:
return abs(end[0] - y) + abs(end[1] - x)
# (distance + heuristic, distance, (start_pos, cycle))
queue = [(heuristic(start[0], start[1]), 0, ((start[0], start[1]), start_cycle))]
visited: set[tuple[tuple[int, int], int]] = set()
distances: dict[tuple[int, int], dict[int, int]] = defaultdict(lambda: {})
while queue:
_, distance, ((y, x), cycle) = heapq.heappop(queue)
if ((y, x), cycle) in visited:
continue
distances[y, x][cycle] = distance
visited.add(((y, x), cycle))
if (y, x) == (end[0], end[1]):
break
for dy, dx in (0, 0), (-1, 0), (1, 0), (0, -1), (0, 1):
ty = y + dy
tx = x + dx
n_cycle = (cycle + 1) % CYCLE
if (ty, tx) == end:
heapq.heappush(queue, (distance + 1, distance + 1, ((ty, tx), n_cycle)))
break
if ((ty, tx), n_cycle) in visited:
continue
if (ty, tx) != start and (ty < 0 or tx < 0 or ty >= n_rows or tx >= n_cols):
continue
if (ty, tx) != start:
if (ty - n_cycle) % n_rows in south_winds[tx]:
continue
if (ty + n_cycle) % n_rows in north_winds[tx]:
continue
if (tx + n_cycle) % n_cols in west_winds[ty]:
continue
if (tx - n_cycle) % n_cols in east_winds[ty]:
continue
heapq.heappush(
queue,
((heuristic(ty, tx) + distance + 1, distance + 1, ((ty, tx), n_cycle))),
)
return distances, next(iter(distances[end].values()))
start = (
-1,
next(j for j in range(1, len(lines[0]) - 1) if lines[0][j] == ".") - 1,
)
end = (
n_rows,
next(j for j in range(1, len(lines[-1]) - 1) if lines[-1][j] == ".") - 1,
)
distances_1, forward_1 = run(start, 0, end)
print(f"answer 1 is {forward_1}")
distances_2, return_1 = run(end, next(iter(distances_1[end].keys())), start)
distances_3, forward_2 = run(start, next(iter(distances_2[start].keys())), end)
print(f"answer 2 is {forward_1 + return_1 + forward_2}")

View File

@@ -1,29 +0,0 @@
# -*- encoding: utf-8 -*-
import sys
lines = sys.stdin.read().splitlines()
coeffs = {"2": 2, "1": 1, "0": 0, "-": -1, "=": -2}
def snafu2number(number: str) -> int:
value = 0
for c in number:
value *= 5
value += coeffs[c]
return value
def number2snafu(number: int) -> str:
values = ["0", "1", "2", "=", "-"]
res = ""
while number > 0:
mod = number % 5
res = res + values[mod]
number = number // 5 + int(mod >= 3)
return "".join(reversed(res))
answer_1 = number2snafu(sum(map(snafu2number, lines)))
print(f"answer 1 is {answer_1}")

View File

@@ -1,25 +0,0 @@
# -*- encoding: utf-8 -*-
import string
import sys
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]
# priorities
priorities = {c: i + 1 for i, c in enumerate(string.ascii_letters)}
# part 1
part1 = sum(priorities[c] for p1, p2 in parts for c in p1.intersection(p2))
print(f"score 1 is {part1}")
# 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"score 2 is {part2}")

View File

@@ -1,19 +0,0 @@
# -*- encoding: utf-8 -*-
import sys
lines = [line.strip() for line in sys.stdin.readlines()]
def make_range(value: str) -> set[int]:
parts = value.split("-")
return set(range(int(parts[0]), int(parts[1]) + 1))
sections = [tuple(make_range(part) for part in line.split(",")) for line in lines]
score_1 = sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections)
print(f"score 1 is {score_1}")
score_2 = sum(bool(s1.intersection(s2)) for s1, s2 in sections)
print(f"score 1 is {score_2}")

View File

@@ -1,43 +0,0 @@
# -*- encoding: utf-8 -*-
import copy
import sys
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()}
# 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()
if crate:
blocks[stack].append(crate)
# part 1 - deep copy for part 2
blocks_1 = copy.deepcopy(blocks)
for move in moves_s:
_, count_s, _, from_, _, to_ = move.strip().split()
for _i in range(int(count_s)):
blocks_1[to_].append(blocks_1[from_].pop())
# part 2
blocks_2 = copy.deepcopy(blocks)
for move in moves_s:
_, count_s, _, from_, _, to_ = move.strip().split()
count = int(count_s)
blocks_2[to_].extend(blocks_2[from_][-count:])
del blocks_2[from_][-count:]
answer_1 = "".join(s[-1] for s in blocks_1.values())
print(f"answer 1 is {answer_1}")
answer_2 = "".join(s[-1] for s in blocks_2.values())
print(f"answer 2 is {answer_2}")

View File

@@ -1,17 +0,0 @@
# -*- encoding: utf-8 -*-
import sys
def index_of_first_n_differents(data: str, n: int) -> int:
for i in range(len(data)):
if len(set(data[i : i + n])) == n:
return i + n
return -1
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,82 +0,0 @@
# -*- encoding: utf-8 -*-
import sys
from pathlib import Path
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
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
answer_1 = sum(size for size in acc_sizes.values() if size <= 100_000)
print(f"answer 1 is {answer_1}")
# 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
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 +0,0 @@
# -*- encoding: utf-8 -*-
import sys
import numpy as np
lines = sys.stdin.read().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.min(axis=2) < trees).sum()
print(f"answer 1 is {answer_1}")
def viewing_distance(row_of_trees: np.ndarray, value: int) -> int:
w = np.where(row_of_trees >= value)[0]
if not w.size:
return len(row_of_trees)
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]),
]
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,45 +0,0 @@
import sys
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(lookups: dict[str, int]) -> list[int]:
values: list[int] = []
for line in filter(bool, lines):
first_digit = min(
lookups,
key=lambda lookup: index
if (index := line.find(lookup)) >= 0
else len(line),
)
last_digit = max(
lookups,
key=lambda lookup: index if (index := line.rfind(lookup)) >= 0 else -1,
)
values.append(10 * lookups[first_digit] + lookups[last_digit])
return values
print(f"answer 1 is {sum(find_values(lookups_1))}")
print(f"answer 2 is {sum(find_values(lookups_2))}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,45 +0,0 @@
import operator
import sys
from functools import reduce
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(":")
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}")
# part 2
answer_2 = sum(
reduce(
operator.mul,
(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,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,53 +0,0 @@
import string
import sys
from collections import defaultdict
NOT_A_SYMBOL = "." + string.digits
lines = sys.stdin.read().splitlines()
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
# 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])
# 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] == "*":
gears[i2, j2].append(value)
if found:
values.append(value)
# continue starting from the end of the number
j = k
# part 1
answer_1 = sum(values)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = sum(v1 * v2 for v1, v2 in filter(lambda vs: len(vs) == 2, gears.values()))
print(f"answer 2 is {answer_2}")

View File

@@ -1,41 +0,0 @@
import sys
from dataclasses import dataclass
@dataclass(frozen=True)
class Card:
id: int
numbers: list[int]
values: list[int]
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()],
)
)
winnings = [sum(1 for n in card.values if n in card.numbers) for card in cards]
# 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))}
for i in range(len(cards)):
card2values[i] += 1
for j in card2cards[i]:
card2values[j] += card2values[i]
print(f"answer 2 is {sum(card2values.values())}")

View File

@@ -1,129 +0,0 @@
import sys
from typing import Sequence
MAP_ORDER = [
"seed",
"soil",
"fertilizer",
"water",
"light",
"temperature",
"humidity",
"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]]
) -> list[tuple[int, int]]:
"""
Given an input range, use the given mapping to find the corresponding list of
ranges in the target domain.
"""
r_start, r_length = values
ranges: list[tuple[int, int]] = []
# find index of the first and last intervals in map that overlaps the input
# interval
index_start, index_end = -1, -1
for index_start, (start, _, length) in enumerate(map):
if start <= r_start and start + length > r_start:
break
for index_end, (start, _, length) in enumerate(
map[index_start:], start=index_start
):
if r_start + r_length >= start and r_start + r_length < start + length:
break
assert index_start >= 0 and index_end >= 0
# special case if one interval contains everything
if index_start == index_end:
start, target, length = map[index_start]
ranges.append((target + r_start - start, r_length))
else:
# add the start interval part
start, target, length = map[index_start]
ranges.append((target + r_start - start, start + length - r_start))
# add all intervals between the first and last (excluding both)
index = index_start + 1
while index < index_end:
start, target, length = map[index]
ranges.append((target, length))
index += 1
# add the last interval
start, target, length = map[index_end]
ranges.append((target, r_start + r_length - start))
return ranges
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}")
# # 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,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,13 +0,0 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -1,7 +1,36 @@
# Advent Of Code # Holt59 - Advent Of Code
To run any script, you need to pipe the input: Installation (with [`poetry`](https://python-poetry.org/)):
```bash ```bash
cat 2022/inputs/day2.txt | python 2022/day2.py poetry install
```
To run any day:
```bash
holt59-aoc $day
```
You can use `-v` / `--verbose` for extra outputs in some case, `-t` / `--test` to run
the code on the test data (one of the test data if multiple are present) or even
`-u XXX` / `--user XXX` to run the code on a specific input after putting the input
file under `src/holt59/aoc/inputs/XXX/$year/$day`.
Full usage:
```bash
usage: Holt59 Advent-Of-Code Runner [-h] [-v] [-t] [-u USER] [-i INPUT] [-y YEAR] day
positional arguments:
day day to run
options:
-h, --help show this help message and exit
-v, --verbose verbose mode
-t, --test test mode
-u USER, --user USER user input to use
-i INPUT, --input INPUT
input to use (override user and test)
-y YEAR, --year YEAR year to run
``` ```

1298
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

43
pyproject.toml Normal file
View File

@@ -0,0 +1,43 @@
[tool.poetry]
name = "holt59-advent-of-code"
version = "0.1.0"
description = ""
authors = ["Mikael CAPELLE <capelle.mikael@gmail.com>"]
license = "MIT"
readme = "README.md"
packages = [{ include = "holt59", from = "src" }]
[tool.poetry.dependencies]
python = "^3.10"
numpy = "^2.1.3"
tqdm = "^4.67.1"
parse = "^1.20.2"
scipy = "^1.14.1"
sympy = "^1.13.3"
networkx = "^3.4.2"
pandas = "^2.2.3"
[tool.poetry.group.dev.dependencies]
pyright = "^1.1.389"
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"
[tool.poe.tasks]
format-imports = "ruff check --select I src --fix"
format-ruff = "ruff format src"
format.sequence = ["format-imports", "format-ruff"]
lint-ruff = "ruff check src"
lint-ruff-format = "ruff format --check src"
lint-pyright = "pyright src"
lint.sequence = ["lint-ruff", "lint-ruff-format", "lint-pyright"]
lint.ignore_fail = "return_non_zero"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

11
run.ps1
View File

@@ -1,11 +0,0 @@
param(
[switch]$Test,
[PSDefaultValue()]
[Parameter(Mandatory = $false)]
$Year = 2023,
[Parameter(Mandatory = $true, Position = 0)]
$Day)
$folder = $Test ? "tests" : "inputs"
Get-Content ".\$Year\$folder\day$Day.txt" | python ".\$Year\day$Day.py"

View File

@@ -0,0 +1,12 @@
from typing import Any, Iterator
from ..base import BaseSolver
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)

View File

@@ -0,0 +1,147 @@
import itertools
from typing import Any, Iterator
from ..base import BaseSolver
# see http://www.se16.info/js/lands2.htm for the explanation of 'atoms' (or elements)
#
# see also https://www.youtube.com/watch?v=ea7lJkEhytA (video link from AOC) and this
# CodeGolf answer https://codegolf.stackexchange.com/a/8479/42148
# fmt: off
ATOMS: list[tuple[str, tuple[int, ...]]] = [
("22", (0, )), # 0
("13112221133211322112211213322112", (71, 90, 0, 19, 2, )), # 1
("312211322212221121123222112", (1, )), # 2
("111312211312113221133211322112211213322112", (31, 19, 2, )), # 3
("1321132122211322212221121123222112", (3, )), # 4
("3113112211322112211213322112", (4, )), # 5
("111312212221121123222112", (5, )), # 6
("132112211213322112", (6, )), # 7
("31121123222112", (7, )), # 8
("111213322112", (8, )), # 9
("123222112", (9, )), # 10
("3113322112", (60, 10, )), # 11
("1113222112", (11, )), # 12
("1322112", (12, )), # 13
("311311222112", (66, 13, )), # 14
("1113122112", (14, )), # 15
("132112", (15, )), # 16
("3112", (16, )), # 17
("1112", (17, )), # 18
("12", (18, )), # 19
("3113112221133112", (66, 90, 0, 19, 26, )), # 20
("11131221131112", (20, )), # 21
("13211312", (21, )), # 22
("31132", (22, )), # 23
("111311222112", (23, 13, )), # 24
("13122112", (24, )), # 25
("32112", (25, )), # 26
("11133112", (29, 26, )), # 27
("131112", (27, )), # 28
("312", (28, )), # 29
("13221133122211332", (62, 19, 88, 0, 19, 29, )), # 30
("31131122211311122113222", (66, 30, )), # 31
("11131221131211322113322112", (31, 10, )), # 32
("13211321222113222112", (32, )), # 33
("3113112211322112", (33, )), # 34
("11131221222112", (34, )), # 35
("1321122112", (35, )), # 36
("3112112", (36, )), # 37
("1112133", (37, 91, )), # 38
("12322211331222113112211", (38, 0, 19, 42, )), # 39
("1113122113322113111221131221", (67, 39, )), # 40
("13211322211312113211", (40, )), # 41
("311322113212221", (41, )), # 42
("132211331222113112211", (62, 19, 42, )), # 43
("311311222113111221131221", (66, 43, )), # 44
("111312211312113211", (44, )), # 45
("132113212221", (45, )), # 46
("3113112211", (46, )), # 47
("11131221", (47, )), # 48
("13211", (48, )), # 49
("3112221", (60, 49, )), # 50
("1322113312211", (62, 19, 50, )), # 51
("311311222113111221", (66, 51, )), # 52
("11131221131211", (52, )), # 53
("13211321", (53, )), # 54
("311311", (54, )), # 55
("11131", (55, )), # 56
("1321133112", (56, 0, 19, 26, )), # 57
("31131112", (57, )), # 58
("111312", (58, )), # 59
("132", (59, )), # 60
("311332", (60, 19, 29, )), # 61
("1113222", (61, )), # 62
("13221133112", (62, 19, 26, )), # 63
("3113112221131112", (66, 63, )), # 64
("111312211312", (64, )), # 65
("1321132", (65, )), # 66
("311311222", (66, 60, )), # 67
("11131221133112", (67, 19, 26, )), # 68
("1321131112", (68, )), # 69
("311312", (69, )), # 70
("11132", (70, )), # 71
("13112221133211322112211213322113", (71, 90, 0, 19, 73, )), # 72
("312211322212221121123222113", (72, )), # 73
("111312211312113221133211322112211213322113", (31, 19, 73, )), # 74
("1321132122211322212221121123222113", (74, )), # 75
("3113112211322112211213322113", (75, )), # 76
("111312212221121123222113", (76, )), # 77
("132112211213322113", (77, )), # 78
("31121123222113", (78, )), # 79
("111213322113", (79, )), # 80
("123222113", (80, )), # 81
("3113322113", (60, 81, )), # 82
("1113222113", (82, )), # 83
("1322113", (83, )), # 84
("311311222113", (66, 84, )), # 85
("1113122113", (85, )), # 86
("132113", (86, )), # 87
("3113", (87, )), # 88
("1113", (88, )), # 89
("13", (89, )), # 90
("3", (90, )), # 91
]
# fmt: on
STARTERS = [
"1",
"11",
"21",
"1211",
"111221",
"312211",
"13112221",
"1113213211",
"31131211131221",
]
def look_and_say_length(s: str, n: int) -> int:
if n == 0:
return len(s)
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[idx] = 1
for _ in range(n):
c2 = {i: 0 for i in range(len(ATOMS))}
for i in counts:
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))
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)

View File

@@ -0,0 +1,49 @@
import itertools
from typing import Any, Iterator
from ..base import BaseSolver
def is_valid(p: str) -> bool:
if any(c in "iol" for c in p):
return False
if not any(
ord(a) + 1 == ord(b) and ord(b) + 1 == ord(c)
for a, b, c in zip(p, p[1:], p[2:])
):
return False
if sum(len(list(g)) >= 2 for _, g in itertools.groupby(p)) < 2:
return False
return True
assert not is_valid("hijklmmn")
assert not is_valid("abbceffg")
assert not is_valid("abbcegjk")
assert is_valid("abcdffaa")
assert is_valid("ghjaabcc")
def increment(p: str) -> str:
if p[-1] == "z":
return increment(p[:-1]) + "a"
elif p[-1] in "iol":
return p[:-1] + chr(ord(p[-1]) + 2)
else:
return p[:-1] + chr(ord(p[-1]) + 1)
def find_next_password(p: str) -> str:
while not is_valid(p):
p = increment(p)
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))

View File

@@ -0,0 +1,27 @@
import json
from typing import Any, Iterator, TypeAlias
from ..base import BaseSolver
JsonObject: TypeAlias = dict[str, "JsonObject"] | list["JsonObject"] | int | str
def json_sum(value: JsonObject, ignore: str | None = None) -> int:
if isinstance(value, str):
return 0
elif isinstance(value, int):
return value
elif isinstance(value, list):
return sum(json_sum(v, ignore=ignore) for v in value)
elif ignore not in value.values():
return sum(json_sum(v, ignore=ignore) for v in value.values())
else:
return 0
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
data: JsonObject = json.loads(input)
yield json_sum(data)
yield json_sum(data, "red")

View File

@@ -0,0 +1,40 @@
import itertools
from collections import defaultdict
from typing import Any, Iterator, 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)
return max(
sum(
happiness[o][d] + happiness[d][o]
for o, d in zip((guests[0],) + order, order + (guests[0],))
)
for order in map(tuple, itertools.permutations(guests[1:]))
)
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.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
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)

View File

@@ -0,0 +1,63 @@
from dataclasses import dataclass
from typing import Any, Iterator, Literal, cast
import parse # type: ignore
from ..base import BaseSolver
@dataclass(frozen=True)
class Reindeer:
name: str
speed: int
fly_time: int
rest_time: int
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.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
)
)
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}
for time in self.progress.wrap(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
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

View File

@@ -0,0 +1,56 @@
import math
from typing import Any, Iterator, Sequence, cast
import parse # type: ignore
from ..base import BaseSolver
def score(ingredients: list[list[int]], teaspoons: Sequence[int]) -> int:
return math.prod(
max(
0,
sum(
ingredient[prop] * teaspoon
for ingredient, teaspoon in zip(ingredients, teaspoons)
),
)
for prop in range(len(ingredients[0]) - 1)
)
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.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.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)
)
)
yield max(scores)
yield max(score for score, calory in zip(scores, calories) if calory == 500)

View File

@@ -0,0 +1,57 @@
import operator as op
import re
from collections import defaultdict
from typing import Any, Callable, Iterator
from ..base import BaseSolver
MFCSAM: dict[str, int] = {
"children": 3,
"cats": 7,
"samoyeds": 2,
"pomeranians": 3,
"akitas": 0,
"vizslas": 0,
"goldfish": 5,
"trees": 3,
"cars": 2,
"perfumes": 1,
}
def match(
aunts: list[dict[str, int]], operators: dict[str, Callable[[int, int], bool]]
) -> int:
return next(
i
for i, aunt in enumerate(aunts, start=1)
if all(operators[k](aunt[k], MFCSAM[k]) for k in aunt)
)
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
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,
),
)

View File

@@ -0,0 +1,34 @@
from typing import Any, Iterator
from ..base import BaseSolver
def iter_combinations(value: int, containers: list[int]) -> Iterator[tuple[int, ...]]:
if value < 0:
return
if value == 0:
yield ()
for i in range(len(containers)):
for combination in iter_combinations(
value - containers[i], containers[i + 1 :]
):
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
combinations = [
combination for combination in iter_combinations(total, containers)
]
yield len(combinations)
min_containers = min(len(combination) for combination in combinations)
yield sum(
1 for combination in combinations if len(combination) == min_containers
)

View File

@@ -0,0 +1,66 @@
import itertools
from typing import Any, Iterator
import numpy as np
from numpy.typing import NDArray
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
grid0 = np.array([[c == "#" for c in line] for line in input.splitlines()])
# add an always off circle around
grid0 = np.concatenate(
[
np.zeros((grid0.shape[0] + 2, 1), dtype=bool),
np.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,
)
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()
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))
return grid
grid = grid0
n_steps = 4 if len(grid) < 10 else 100
for _ in range(n_steps):
grid = game_of_life(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[[1, 1, -2, -2], [1, -2, 1, -2]] = True
yield sum(cell for line in grid for cell in line)

View File

@@ -0,0 +1,58 @@
from collections import defaultdict
from typing import Any, Iterator
from ..base import BaseSolver
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

View File

@@ -0,0 +1,24 @@
from typing import Any, Iterator
import numpy as np
from ..base import BaseSolver
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)
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
)
)

View File

@@ -0,0 +1,29 @@
import itertools
from typing import Any, Iterator
from ..base import BaseSolver
def presents(n: int, elf: int, max: int) -> int:
count = 0
k = 1
while k * k < n:
if n % k == 0:
if n // k <= max:
count += elf * k
if k <= max:
count += elf * (n // k)
k += 1
if k * k == n and k <= max:
count += elf * k
return count
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
target = int(input)
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)

View File

@@ -0,0 +1,64 @@
import itertools
from math import ceil
from typing import Any, Iterator, TypeAlias
from ..base import BaseSolver
Modifier: TypeAlias = tuple[str, int, int, int]
WEAPONS: list[Modifier] = [
("Dagger", 8, 4, 0),
("Shortsword", 10, 5, 0),
("Warhammer", 25, 6, 0),
("Longsword", 40, 7, 0),
("Greataxe", 74, 8, 0),
]
ARMORS: list[Modifier] = [
("", 0, 0, 0),
("Leather", 13, 0, 1),
("Chainmail", 31, 0, 2),
("Splintmail", 53, 0, 3),
("Bandedmail", 75, 0, 4),
("Platemail", 102, 0, 5),
]
RINGS: list[Modifier] = [
("", 0, 0, 0),
("Damage +1", 25, 1, 0),
("Damage +2", 50, 2, 0),
("Damage +3", 100, 3, 0),
("Defense +1", 20, 0, 1),
("Defense +2", 40, 0, 2),
("Defense +3", 80, 0, 3),
]
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
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())
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)
)
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)
yield min_cost
yield max_cost

View File

@@ -0,0 +1,182 @@
from __future__ import annotations
import heapq
from typing import Any, Iterator, Literal, TypeAlias, cast
from ..base import BaseSolver
PlayerType: TypeAlias = Literal["player", "boss"]
SpellType: TypeAlias = Literal["magic missile", "drain", "shield", "poison", "recharge"]
BuffType: TypeAlias = Literal["shield", "poison", "recharge"]
Node: TypeAlias = tuple[
PlayerType,
int,
int,
int,
int,
int,
tuple[tuple[BuffType, int], ...],
tuple[tuple[SpellType, int], ...],
]
ATTACK_SPELLS: list[tuple[SpellType, int, int, int]] = [
("magic missile", 53, 4, 0),
("drain", 73, 2, 2),
]
BUFF_SPELLS: list[tuple[BuffType, int, int]] = [
("shield", 113, 6),
("poison", 173, 6),
("recharge", 229, 5),
]
def play(
player_hp: int,
player_mana: int,
player_armor: int,
boss_hp: int,
boss_attack: int,
hard_mode: bool,
) -> tuple[tuple[SpellType, int], ...]:
winning_node: tuple[tuple[SpellType, int], ...] | None = None
visited: set[
tuple[PlayerType, int, int, int, int, tuple[tuple[BuffType, int], ...]]
] = set()
nodes: list[Node] = [
("player", 0, player_hp, player_mana, player_armor, boss_hp, (), ())
]
while winning_node is None:
(
player,
mana,
player_hp,
player_mana,
player_armor,
boss_hp,
buffs,
spells,
) = heapq.heappop(nodes)
if (player, player_hp, player_mana, player_armor, boss_hp, buffs) in visited:
continue
visited.add((player, player_hp, player_mana, player_armor, boss_hp, buffs))
new_buffs: list[tuple[BuffType, int]] = []
for buff, length in buffs:
length = length - 1
match buff:
case "poison":
boss_hp = max(boss_hp - 3, 0)
case "shield":
if length == 0:
player_armor -= 7
case "recharge":
player_mana += 101
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":
heapq.heappush(
nodes,
(
"player",
mana,
max(0, player_hp - max(boss_attack - player_armor, 1)),
player_mana,
player_armor,
boss_hp,
buffs,
spells,
),
)
else:
buff_types = {b for b, _ in buffs}
for spell, cost, damage, regeneration in ATTACK_SPELLS:
if player_mana < cost:
continue
heapq.heappush(
nodes,
(
"boss",
mana + cost,
player_hp + regeneration,
player_mana - cost,
player_armor,
max(0, boss_hp - damage),
buffs,
spells + cast("tuple[tuple[SpellType, int]]", ((spell, cost),)),
),
)
for buff_type, buff_cost, buff_length in BUFF_SPELLS:
if buff_type in buff_types:
continue
if player_mana < buff_cost:
continue
heapq.heappush(
nodes,
(
"boss",
mana + buff_cost,
player_hp,
player_mana - buff_cost,
player_armor + 7 * (buff_type == "shield"),
boss_hp,
buffs
+ cast(
"tuple[tuple[BuffType, int]]", ((buff_type, buff_length),)
),
spells
+ cast(
"tuple[tuple[SpellType, int]]", ((buff_type, buff_cost),)
),
),
)
return winning_node
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
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())
yield sum(
c
for _, c in play(
player_hp, player_mana, player_armor, boss_hp, boss_attack, False
)
)
# 1242 (not working)
yield sum(
c
for _, c in play(
player_hp, player_mana, player_armor, boss_hp, boss_attack, True
)
)

View File

@@ -0,0 +1,33 @@
from collections import defaultdict
from typing import Any, Iterator
from ..base import BaseSolver
def process(directions: str) -> dict[tuple[int, int], int]:
counts: dict[tuple[int, int], int] = defaultdict(lambda: 0)
counts[0, 0] = 1
x, y = (0, 0)
for c in directions:
match c:
case ">":
x += 1
case "<":
x -= 1
case "^":
y -= 1
case "v":
y += 1
case _:
raise ValueError()
counts[x, y] += 1
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]))

View File

@@ -0,0 +1,20 @@
import hashlib
import itertools
from typing import Any, Iterator
from ..base import BaseSolver
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")
)

View File

@@ -0,0 +1,36 @@
from typing import Any, Iterator
from ..base import BaseSolver
VOWELS = "aeiou"
FORBIDDEN = {"ab", "cd", "pq", "xy"}
def is_nice_1(s: str) -> bool:
if sum(c in VOWELS for c in s) < 3:
return False
if not any(a == b for a, b in zip(s[:-1:], s[1::])):
return False
if any(s.find(f) >= 0 for f in FORBIDDEN):
return False
return True
def is_nice_2(s: str) -> bool:
if not any(s.find(s[i : i + 2], i + 2) >= 0 for i in range(len(s))):
return False
if not any(a == b for a, b in zip(s[:-1:], s[2::])):
return False
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))

View File

@@ -0,0 +1,32 @@
from typing import Any, Iterator, Literal, cast
import numpy as np
import parse # type: ignore
from ..base import BaseSolver
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
yield lights_1.sum()
yield lights_2.sum()

View File

@@ -0,0 +1,96 @@
import operator
from typing import Any, Callable, Iterator
from ..base import BaseSolver
OPERATORS = {
"AND": operator.and_,
"OR": operator.or_,
"LSHIFT": operator.lshift,
"RSHIFT": operator.rshift,
}
ValueGetter = Callable[[dict[str, int]], int]
Signals = dict[
str,
tuple[
tuple[str, str],
tuple[ValueGetter, ValueGetter],
Callable[[int, int], int],
],
]
def zero_op(_a: int, _b: int) -> int:
return 0
def value_of(key: str) -> tuple[str, Callable[[dict[str, int]], int]]:
try:
return "", lambda _p, _v=int(key): _v
except ValueError:
return key, lambda values: values[key]
def process(
signals: Signals,
values: dict[str, int],
) -> dict[str, int]:
while signals:
signal = next(s for s in signals if all(p in values for p in signals[s][0]))
_, deps, command = signals[signal]
values[signal] = command(deps[0](values), deps[1](values)) % 65536
del signals[signal]
return values
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any] | None:
lines = input.splitlines()
signals: Signals = {}
values: dict[str, int] = {"": 0}
for line in lines:
command, signal = line.split(" -> ")
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"]

View File

@@ -0,0 +1,32 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.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
)
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
)

View File

@@ -0,0 +1,28 @@
import itertools
from collections import defaultdict
from typing import Any, Iterator, cast
import parse # type: ignore
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
distances: dict[str, dict[str, int]] = defaultdict(dict)
for line in lines:
origin, destination, length = cast(
tuple[str, str, int],
parse.parse("{} to {} = {:d}", line), # type: ignore
)
distances[origin][destination] = distances[destination][origin] = length
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())

View File

@@ -0,0 +1,17 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
values = [int(line) for line in lines]
# part 1
yield sum(v2 > v1 for v1, v2 in zip(values[:-1], values[1:]))
# part 2
runnings = [sum(values[i : i + 3]) for i in range(len(values) - 2)]
yield sum(v2 > v1 for v1, v2 in zip(runnings[:-1], runnings[1:]))

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,38 @@
from math import prod
from typing import Any, Iterator, Literal, TypeAlias, cast
from ..base import BaseSolver
Command: TypeAlias = Literal["forward", "up", "down"]
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
commands: list[tuple[Command, int]] = [
(cast(Command, (p := line.split())[0]), int(p[1])) for line in lines
]
def depth_and_position(use_aim: bool):
aim, pos, depth = 0, 0, 0
for command, value in commands:
d_depth = 0
match command:
case "forward":
pos += value
depth += value * aim
case "up":
d_depth = -value
case "down":
d_depth = value
if use_aim:
aim += d_depth
else:
depth += value
return depth, pos
yield prod(depth_and_position(False))
yield prod(depth_and_position(True))

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,7 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]: ...

View File

@@ -0,0 +1,43 @@
from collections import Counter
from typing import Any, Iterator, Literal
from ..base import BaseSolver
def generator_rating(
values: list[str], most_common: bool, default: Literal["0", "1"]
) -> str:
index = 0
most_common_idx = 0 if most_common else 1
while len(values) > 1:
cnt = Counter(value[index] for value in values)
bit = cnt.most_common(2)[most_common_idx][0]
if cnt["0"] == cnt["1"]:
bit = default
values = [value for value in values if value[index] == bit]
index += 1
return values[0]
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
# part 1
most_and_least_common = [
tuple(
Counter(line[col] for line in lines).most_common(2)[m][0]
for m in range(2)
)
for col in range(len(lines[0]))
]
gamma_rate = int("".join(most for most, _ in most_and_least_common), base=2)
epsilon_rate = int("".join(least for _, least in most_and_least_common), base=2)
yield gamma_rate * epsilon_rate
# part 2
oxygen_generator_rating = int(generator_rating(lines, True, "1"), base=2)
co2_scrubber_rating = int(generator_rating(lines, False, "0"), base=2)
yield oxygen_generator_rating * co2_scrubber_rating

View File

@@ -0,0 +1,52 @@
from typing import Any, Iterator
import numpy as np
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
numbers = [int(c) for c in lines[0].split(",")]
boards = np.asarray(
[
[[int(c) for c in line.split()] for line in lines[start : start + 5]]
for start in range(2, len(lines), 6)
]
)
# (round, score) for each board (-1 when not found)
winning_rounds: list[tuple[int, int]] = [(-1, -1) for _ in range(len(boards))]
marked = np.zeros_like(boards, dtype=bool)
for round, number in enumerate(numbers):
# mark boards
marked[boards == number] = True
# check each board for winning
for index in range(len(boards)):
if winning_rounds[index][0] > 0:
continue
if np.any(
np.all(marked[index], axis=0) | np.all(marked[index], axis=1)
):
winning_rounds[index] = (
round,
number * int(np.sum(boards[index][~marked[index]])),
)
# all boards are winning - break
if np.all(marked.all(axis=1) | marked.all(axis=2)):
break
# part 1
(_, score) = min(winning_rounds, key=lambda w: w[0])
yield score
# part 2
(_, score) = max(winning_rounds, key=lambda w: w[0])
yield score

View File

@@ -0,0 +1,48 @@
from typing import Any, Iterator
import numpy as np
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
sections: list[tuple[tuple[int, int], tuple[int, int]]] = [
(
(
int(line.split(" -> ")[0].split(",")[0]),
int(line.split(" -> ")[0].split(",")[1]),
),
(
int(line.split(" -> ")[1].split(",")[0]),
int(line.split(" -> ")[1].split(",")[1]),
),
)
for line in lines
]
np_sections = np.array(sections).reshape(-1, 4)
x_max, y_max = (
max(np_sections[:, 0].max(), np_sections[:, 2].max()),
max(np_sections[:, 1].max(), np_sections[:, 3].max()),
)
counts_1 = np.zeros((y_max + 1, x_max + 1), dtype=int)
counts_2 = counts_1.copy()
for (x1, y1), (x2, y2) in sections:
x_rng = range(x1, x2 + 1, 1) if x2 >= x1 else range(x1, x2 - 1, -1)
y_rng = range(y1, y2 + 1, 1) if y2 >= y1 else range(y1, y2 - 1, -1)
if x1 == x2 or y1 == y2:
counts_1[list(y_rng), list(x_rng)] += 1
counts_2[list(y_rng), list(x_rng)] += 1
elif abs(x2 - x1) == abs(y2 - y1):
for i, j in zip(y_rng, x_rng):
counts_2[i, j] += 1
yield (counts_1 >= 2).sum()
yield (counts_2 >= 2).sum()

View File

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

View File

@@ -0,0 +1,22 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
positions = [int(c) for c in input.split(",")]
min_position, max_position = min(positions), max(positions)
# part 1
yield min(
sum(abs(p - position) for p in positions)
for position in range(min_position, max_position + 1)
)
# part 2
yield min(
sum(abs(p - position) * (abs(p - position) + 1) // 2 for p in positions)
for position in range(min_position, max_position + 1)
)

View File

@@ -0,0 +1,89 @@
import itertools
from typing import Any, Iterator
from ..base import BaseSolver
digits = {
"abcefg": 0,
"cf": 1,
"acdeg": 2,
"acdfg": 3,
"bcdf": 4,
"abdfg": 5,
"abdefg": 6,
"acf": 7,
"abcdefg": 8,
"abcdfg": 9,
}
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
# part 1
lengths = {len(k) for k, v in digits.items() if v in (1, 4, 7, 8)}
yield sum(
len(p) in lengths
for line in lines
for p in line.split("|")[1].strip().split()
)
# part 2
values: list[int] = []
for line in lines:
parts = line.split("|")
broken_digits = sorted(parts[0].strip().split(), key=len)
per_length = {
k: list(v)
for k, v in itertools.groupby(sorted(broken_digits, key=len), key=len)
}
# a can be found immediately
a = next(u for u in per_length[3][0] if u not in per_length[2][0])
# c and f have only two possible values corresponding to the single entry of
# length 2
cf = list(per_length[2][0])
# the only digit of length 4 contains bcdf, so we can deduce bd by removing cf
bd = [u for u in per_length[4][0] if u not in cf]
# the 3 digits of length 5 have a, d and g in common
adg = [
u for u in per_length[5][0] if all(u in pe for pe in per_length[5][1:])
]
# we can remove a
dg = [u for u in adg if u != a]
# we can deduce d and g
d = next(u for u in dg if u in bd)
g = next(u for u in dg if u != d)
# then b
b = next(u for u in bd if u != d)
# f is in the three 6-length digits, while c is only in 2
f = next(u for u in cf if all(u in p for p in per_length[6]))
# c is not f
c = next(u for u in cf if u != f)
# e is the last one
e = next(u for u in "abcdefg" if u not in {a, b, c, d, f, g})
mapping = dict(zip((a, b, c, d, e, f, g), "abcdefg"))
value = 0
for number in parts[1].strip().split():
digit = "".join(sorted(mapping[c] for c in number))
value = 10 * value + digits[digit]
self.logger.info(f"value for '{line}' is {value}")
values.append(value)
yield sum(values)

View File

@@ -0,0 +1,47 @@
from math import prod
from typing import Any, Iterator
from ..base import BaseSolver
def neighbors(point: tuple[int, int], n_rows: int, n_cols: int):
i, j = point
for di, dj in ((-1, 0), (+1, 0), (0, -1), (0, +1)):
if 0 <= i + di < n_rows and 0 <= j + dj < n_cols:
yield (i + di, j + dj)
def basin(values: list[list[int]], start: tuple[int, int]) -> set[tuple[int, int]]:
n_rows, n_cols = len(values), len(values[0])
visited: set[tuple[int, int]] = set()
queue = [start]
while queue:
i, j = queue.pop()
if (i, j) in visited or values[i][j] == 9:
continue
visited.add((i, j))
queue.extend(neighbors((i, j), n_rows, n_cols))
return visited
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
values = [[int(c) for c in row] for row in input.splitlines()]
n_rows, n_cols = len(values), len(values[0])
low_points = [
(i, j)
for i in range(n_rows)
for j in range(n_cols)
if all(
values[ti][tj] > values[i][j]
for ti, tj in neighbors((i, j), n_rows, n_cols)
)
]
yield sum(values[i][j] + 1 for i, j in low_points)
yield prod(sorted(len(basin(values, point)) for point in low_points)[-3:])

View File

@@ -0,0 +1,12 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
blocks = input.split("\n\n")
values = sorted(sum(map(int, block.split())) for block in blocks)
yield values[-1]
yield sum(values[-3:])

View File

@@ -0,0 +1,43 @@
from typing import Any, Iterator
from ..base import BaseSolver
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
cycle, x = 1, 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))
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"
)

View File

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

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