71 Commits

Author SHA1 Message Date
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
404 changed files with 30195 additions and 872 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
__pycache__
.ruff_cache
.vscode
build

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,270 +0,0 @@
# -*- encoding: utf-8 -*-
from __future__ import annotations
import heapq
import itertools
import re
import sys
import time as time_p
from collections import defaultdict
from typing import FrozenSet, NamedTuple
from tqdm import tqdm, trange
class Pipe(NamedTuple):
name: str
flow: int
tunnels: list[str]
def __lt__(self, other: object) -> bool:
return isinstance(other, Pipe) and other.name < self.name
def __eq__(self, other: object) -> bool:
return isinstance(other, Pipe) and other.name == self.name
def __hash__(self) -> int:
return hash(self.name)
def __str__(self) -> str:
return self.name
def __repr__(self) -> str:
return self.name
def breadth_first_search(pipes: dict[str, Pipe], pipe: Pipe) -> dict[Pipe, int]:
"""
Runs a BFS from the given pipe and return the shortest distance (in term of hops)
to all other pipes.
"""
queue = [(0, pipe_1)]
visited = set()
distances: dict[Pipe, int] = {}
while len(distances) < len(pipes):
distance, current = heapq.heappop(queue)
if current in visited:
continue
visited.add(current)
distances[current] = distance
for tunnel in current.tunnels:
heapq.heappush(queue, (distance + 1, pipes[tunnel]))
return distances
def update_with_better(
node_at_times: dict[FrozenSet[Pipe], int], flow: int, flowing: FrozenSet[Pipe]
) -> None:
node_at_times[flowing] = max(node_at_times[flowing], flow)
def part_1(
start_pipe: Pipe,
max_time: int,
distances: dict[tuple[Pipe, Pipe], int],
relevant_pipes: FrozenSet[Pipe],
):
node_at_times: dict[int, dict[Pipe, dict[FrozenSet[Pipe], int]]] = defaultdict(
lambda: defaultdict(lambda: defaultdict(lambda: 0))
)
node_at_times[0] = {start_pipe: {frozenset(): 0}}
for time in range(max_time):
for c_pipe, nodes in node_at_times[time].items():
for flowing, flow in nodes.items():
for target in relevant_pipes:
distance = distances[c_pipe, target] + 1
if time + distance >= max_time or target in flowing:
continue
update_with_better(
node_at_times[time + distance][target],
flow + sum(pipe.flow for pipe in flowing) * distance,
flowing | {target},
)
update_with_better(
node_at_times[max_time][c_pipe],
flow + sum(pipe.flow for pipe in flowing) * (max_time - time),
flowing,
)
return max(
flow
for nodes_of_pipe in node_at_times[max_time].values()
for flow in nodes_of_pipe.values()
)
def part_2(
start_pipe: Pipe,
max_time: int,
pipes: dict[str, Pipe],
relevant_pipes: FrozenSet[Pipe],
distances: dict[tuple[Pipe, Pipe], int],
):
node_at_times: dict[
int, dict[tuple[Pipe, Pipe], dict[FrozenSet[Pipe], int]]
] = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: 0)))
node_at_times[0] = {(start_pipe, start_pipe): {frozenset(): 0}}
# map node + distance to
d1, d2, d3, d4 = 0, 0, 0, 0
best_flow = 0
for time in range(max_time):
print(
f"{time + 1:2d}/{max_time} - {best_flow:4d} - "
f"{sum(map(len, node_at_times[time].values())):7d} - "
f"{d1:.3f} {d2:.3f} {d3:.3f} {d4:.3f}"
)
d1, d2, d3, d4 = 0, 0, 0, 0
for (c_pipe, e_pipe), nodes in node_at_times[time].items():
for flowing, flow in nodes.items():
t1 = time_p.time()
c_best_flow = (
flow
+ sum(pipe.flow for pipe in flowing) * (max_time - time)
+ sum(
(
pipe.flow
* (
max_time
- time
- 1
- min(distances[c_pipe, pipe], distances[e_pipe, pipe])
)
for pipe in relevant_pipes
if pipe not in flowing
),
start=0,
)
)
d1 += time_p.time() - t1
if c_best_flow < best_flow:
continue
best_flow = max(
best_flow,
flow + sum(pipe.flow for pipe in flowing) * (max_time - time),
)
t1 = time_p.time()
if flowing != relevant_pipes:
for c_next_s, e_next_s in itertools.product(
c_pipe.tunnels, e_pipe.tunnels
):
c_next = pipes[c_next_s]
e_next = pipes[e_next_s]
update_with_better(
node_at_times[time + 1][c_next, e_next],
flow + sum(pipe.flow for pipe in flowing),
flowing,
)
d2 += time_p.time() - t1
t1 = time_p.time()
if c_pipe in relevant_pipes and c_pipe not in flowing:
for e_next_s in e_pipe.tunnels:
e_next = pipes[e_next_s]
update_with_better(
node_at_times[time + 1][c_pipe, e_next],
flow + sum(pipe.flow for pipe in flowing),
flowing | {c_pipe},
)
if e_pipe in relevant_pipes and e_pipe not in flowing:
for c_next_s in c_pipe.tunnels:
c_next = pipes[c_next_s]
update_with_better(
node_at_times[time + 1][c_next, e_pipe],
flow + sum(pipe.flow for pipe in flowing),
flowing | {e_pipe},
)
if (
e_pipe in relevant_pipes
and c_pipe in relevant_pipes
and e_pipe not in flowing
and c_pipe not in flowing
):
update_with_better(
node_at_times[time + 1][c_pipe, e_pipe],
flow + sum(pipe.flow for pipe in flowing),
flowing | {c_pipe, e_pipe},
)
update_with_better(
node_at_times[max_time][c_pipe, e_pipe],
flow + sum(pipe.flow for pipe in flowing) * (max_time - time),
flowing,
)
d3 += time_p.time() - t1
return max(
flow
for nodes_of_pipe in node_at_times[max_time].values()
for flow in nodes_of_pipe.values()
)
# === MAIN ===
lines = sys.stdin.read().splitlines()
pipes: dict[str, Pipe] = {}
for line in lines:
r = re.match(
R"Valve ([A-Z]+) has flow rate=([0-9]+); tunnels? leads? to valves? (.+)",
line,
)
assert r
g = r.groups()
pipes[g[0]] = Pipe(g[0], int(g[1]), g[2].split(", "))
# compute distances from one valve to any other
distances: dict[tuple[Pipe, Pipe], int] = {}
for pipe_1 in pipes.values():
distances.update(
{
(pipe_1, pipe_2): distance
for pipe_2, distance in breadth_first_search(pipes, pipe_1).items()
}
)
# valves with flow
relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0)
# 1651, 1653
print(part_1(pipes["AA"], 30, distances, relevant_pipes))
# 1707, 2223
print(part_2(pipes["AA"], 26, pipes, relevant_pipes, distances))

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,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,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
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,14 @@
import sys
lines = sys.stdin.read().splitlines()
values = [int(line) for line in lines]
# part 1
answer_1 = sum(v2 > v1 for v1, v2 in zip(values[:-1], values[1:]))
print(f"answer 1 is {answer_1}")
# part 2
runnings = [sum(values[i : i + 3]) for i in range(len(values) - 2)]
answer_2 = sum(v2 > v1 for v1, v2 in zip(runnings[:-1], runnings[1:]))
print(f"answer 2 is {answer_2}")

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -0,0 +1,41 @@
import sys
from math import prod
from typing import Literal, TypeAlias, cast
lines = sys.stdin.read().splitlines()
Command: TypeAlias = Literal["forward", "up", "down"]
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
# part 1
answer_1 = prod(depth_and_position(False))
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = prod(depth_and_position(True))
print(f"answer 2 is {answer_2}")

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -1,6 +1,4 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()

View File

@@ -0,0 +1,39 @@
import sys
from collections import Counter
from typing import Literal
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]
lines = sys.stdin.read().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)
print(f"answer 1 is {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)
answer_2 = oxygen_generator_rating * co2_scrubber_rating
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,45 @@
import sys
import numpy as np
lines = sys.stdin.read().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])
print(f"answer 1 is {score}")
# part 2
(_, score) = max(winning_rounds, key=lambda w: w[0])
print(f"answer 2 is {score}")

View File

@@ -1,7 +1,4 @@
# -*- encoding: utf-8 -*-
import sys
from collections import defaultdict
import numpy as np
@@ -34,7 +31,6 @@ 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)

View File

@@ -0,0 +1,21 @@
import sys
values = [int(c) for c in sys.stdin.read().strip().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]
# part 1
answer_1 = sum(v for k, v in lanterns.items() if k < 80) + len(values)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = sum(lanterns.values()) + len(values)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,19 @@
import sys
positions = [int(c) for c in sys.stdin.read().strip().split(",")]
min_position, max_position = min(positions), max(positions)
# part 1
answer_1 = min(
sum(abs(p - position) for p in positions)
for position in range(min_position, max_position + 1)
)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = min(
sum(abs(p - position) * (abs(p - position) + 1) // 2 for p in positions)
for position in range(min_position, max_position + 1)
)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,87 @@
import itertools
import os
import sys
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
digits = {
"abcefg": 0,
"cf": 1,
"acdeg": 2,
"acdfg": 3,
"bcdf": 4,
"abdfg": 5,
"abdefg": 6,
"acf": 7,
"abcdefg": 8,
"abcdfg": 9,
}
lines = sys.stdin.read().splitlines()
# part 1
lengths = {len(k) for k, v in digits.items() if v in (1, 4, 7, 8)}
answer_1 = sum(
len(p) in lengths for line in lines for p in line.split("|")[1].strip().split()
)
print(f"answer 1 is {answer_1}")
# part 2
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]
if VERBOSE:
print(value)
values.append(value)
answer_2 = sum(values)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,44 @@
import sys
from math import prod
values = [[int(c) for c in row] for row in sys.stdin.read().splitlines()]
n_rows, n_cols = len(values), len(values[0])
def neighbors(point: tuple[int, 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(start: tuple[int, int]) -> set[tuple[int, int]]:
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)))
return visited
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)))
]
# part 1
answer_1 = sum(values[i][j] + 1 for i, j in low_points)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = prod(sorted(len(basin(point)) for point in low_points)[-3:])
print(f"answer 2 is {answer_2}")

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import sys
blocks = sys.stdin.read().split("\n\n")

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import sys
lines = sys.stdin.read().splitlines()

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import copy
import sys
from functools import reduce
@@ -7,7 +5,6 @@ from typing import Callable, Final, Mapping, Sequence
class Monkey:
id: Final[int]
items: Final[Sequence[int]]
worry_fn: Final[Callable[[int], int]]
@@ -97,8 +94,7 @@ def run(
# number of inspects
inspects = {monkey: 0 for monkey in monkeys}
for round in range(n_rounds):
for _ in range(n_rounds):
for monkey in monkeys:
for item in items[monkey]:
inspects[monkey] += 1

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import heapq
import sys
from typing import Callable, Iterator, TypeVar
@@ -44,7 +42,6 @@ def dijkstra(
visited.add(current)
for neighbor in neighbors(current):
if neighbor in visited:
continue
@@ -60,7 +57,6 @@ def dijkstra(
def make_path(parents: dict[Node, Node], start: Node, end: Node) -> list[Node] | None:
if end not in parents:
return None
@@ -109,7 +105,6 @@ def neighbors(
(c_row, c_col - 1),
(c_row, c_col + 1),
):
if not (n_row >= 0 and n_row < n_rows and n_col >= 0 and n_col < n_cols):
continue
@@ -127,8 +122,8 @@ lines = sys.stdin.read().splitlines()
grid = [[ord(cell) - ord("a") for cell in line] for line in lines]
start: tuple[int, int]
end: tuple[int, int]
start: tuple[int, int] | None = None
end: tuple[int, int] | None = None
# for part 2
start_s: list[tuple[int, int]] = []
@@ -143,6 +138,9 @@ for i_row, row in enumerate(grid):
elif col == 0:
start_s.append((i_row, i_col))
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")

View File

@@ -1,27 +1,27 @@
# -*- encoding: utf-8 -*-
import json
import sys
from functools import cmp_to_key
from typing import TypeAlias, cast
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"]]
def compare(lhs: list[int | list], rhs: list[int | list]) -> int:
def compare(lhs: Packet, rhs: Packet) -> 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]
lhs_a = [lhs_a] # type: ignore
elif not isinstance(rhs_a, list):
rhs_a = [rhs_a]
rhs_a = [rhs_a] # type: ignore
assert isinstance(rhs_a, list) and isinstance(lhs_a, list)
r = compare(lhs_a, rhs_a)
r = compare(cast(Packet, lhs_a), cast(Packet, rhs_a))
if r != 0:
return r

View File

@@ -1,7 +1,4 @@
# -*- encoding: utf-8 -*-
import sys
from collections import defaultdict
from enum import Enum, auto
from typing import Callable, cast
@@ -23,10 +20,10 @@ def print_blocks(blocks: dict[tuple[int, int], Cell]):
blocks: Set of blocks to print.
"""
x_min, y_min, x_max, y_max = (
min(x for x, y in blocks),
min(x for x, _ in blocks),
0,
max(x for x, y in blocks),
max(y for x, y in blocks),
max(x for x, _ in blocks),
max(y for _, y in blocks),
)
for y in range(y_min, y_max + 1):
@@ -56,13 +53,12 @@ def flow(
The input blocks.
"""
y_max = max(y for x, y in blocks)
y_max = max(y for _, 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:
@@ -117,10 +113,10 @@ print_blocks(blocks)
print()
x_min, y_min, x_max, y_max = (
min(x for x, y in blocks),
min(x for x, _ in blocks),
0,
max(x for x, y in blocks),
max(y for x, y in blocks),
max(x for x, _ in blocks),
max(y for _, y in blocks),
)
# === part 1 ===

View File

@@ -0,0 +1,96 @@
import sys
from typing import Any, Iterator
import numpy as np
import parse # type: ignore
from numpy.typing import NDArray
from ..base import BaseSolver
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"])
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
def solve(self, input: str) -> Iterator[Any]:
lines = input.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
yield self.part1(sensor_to_beacon, row)
# 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

View File

@@ -0,0 +1,158 @@
from __future__ import annotations
import heapq
import itertools
import re
import sys
from collections import defaultdict
from typing import FrozenSet, NamedTuple
from tqdm import tqdm
class Pipe(NamedTuple):
name: str
flow: int
tunnels: list[str]
def __lt__(self, other: object) -> bool:
return isinstance(other, Pipe) and other.name < self.name
def __eq__(self, other: object) -> bool:
return isinstance(other, Pipe) and other.name == self.name
def __hash__(self) -> int:
return hash(self.name)
def __str__(self) -> str:
return self.name
def __repr__(self) -> str:
return self.name
def breadth_first_search(pipes: dict[str, Pipe], pipe: Pipe) -> dict[Pipe, int]:
"""
Runs a BFS from the given pipe and return the shortest distance (in term of hops)
to all other pipes.
"""
queue = [(0, pipe_1)]
visited = set()
distances: dict[Pipe, int] = {}
while len(distances) < len(pipes):
distance, current = heapq.heappop(queue)
if current in visited:
continue
visited.add(current)
distances[current] = distance
for tunnel in current.tunnels:
heapq.heappush(queue, (distance + 1, pipes[tunnel]))
return distances
def update_with_better(
node_at_times: dict[FrozenSet[Pipe], int], flow: int, flowing: FrozenSet[Pipe]
) -> None:
node_at_times[flowing] = max(node_at_times[flowing], flow)
def part_1(
start_pipe: Pipe,
max_time: int,
distances: dict[tuple[Pipe, Pipe], int],
relevant_pipes: FrozenSet[Pipe],
):
node_at_times: dict[int, dict[Pipe, dict[FrozenSet[Pipe], int]]] = defaultdict(
lambda: defaultdict(lambda: defaultdict(lambda: 0))
)
node_at_times[0] = {start_pipe: {frozenset(): 0}}
for time in range(max_time):
for c_pipe, nodes in node_at_times[time].items():
for flowing, flow in nodes.items():
for target in relevant_pipes:
distance = distances[c_pipe, target] + 1
if time + distance >= max_time or target in flowing:
continue
update_with_better(
node_at_times[time + distance][target],
flow + sum(pipe.flow for pipe in flowing) * distance,
flowing | {target},
)
update_with_better(
node_at_times[max_time][c_pipe],
flow + sum(pipe.flow for pipe in flowing) * (max_time - time),
flowing,
)
return max(
flow
for nodes_of_pipe in node_at_times[max_time].values()
for flow in nodes_of_pipe.values()
)
def part_2(
start_pipe: Pipe,
max_time: int,
distances: dict[tuple[Pipe, Pipe], int],
relevant_pipes: FrozenSet[Pipe],
):
def compute(pipes_for_me: FrozenSet[Pipe]) -> int:
return part_1(start_pipe, max_time, distances, pipes_for_me) + part_1(
start_pipe, max_time, distances, relevant_pipes - pipes_for_me
)
combs = [
frozenset(relevant_pipes_1)
for r in range(2, len(relevant_pipes) // 2 + 1)
for relevant_pipes_1 in itertools.combinations(relevant_pipes, r)
]
return max(compute(comb) for comb in tqdm(combs))
# === MAIN ===
lines = sys.stdin.read().splitlines()
pipes: dict[str, Pipe] = {}
for line in lines:
r = re.match(
R"Valve ([A-Z]+) has flow rate=([0-9]+); tunnels? leads? to valves? (.+)",
line,
)
assert r
g = r.groups()
pipes[g[0]] = Pipe(g[0], int(g[1]), g[2].split(", "))
# compute distances from one valve to any other
distances: dict[tuple[Pipe, Pipe], int] = {}
for pipe_1 in pipes.values():
distances.update(
{
(pipe_1, pipe_2): distance
for pipe_2, distance in breadth_first_search(pipes, pipe_1).items()
}
)
# valves with flow
relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0)
# 1651, 1653
print(part_1(pipes["AA"], 30, distances, relevant_pipes))
# 1707, 2223
print(part_2(pipes["AA"], 26, distances, relevant_pipes))

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import sys
from typing import Sequence, TypeVar
@@ -49,7 +47,6 @@ def build_tower(
early_stop: bool = False,
init: np.ndarray = np.ones(WIDTH, dtype=bool),
) -> tuple[np.ndarray, int, int, dict[int, int]]:
tower = EMPTY_BLOCKS.copy()
tower[0, :] = init
@@ -59,7 +56,6 @@ def build_tower(
rock_count = 0
for rock_count in range(n_rocks):
if early_stop:
if i_rock == 0 and (i_rock, i_jet) in done_at:
break
@@ -75,7 +71,6 @@ def build_tower(
tower = np.concatenate([tower, EMPTY_BLOCKS], axis=0)
while True:
jet, i_jet = next_cycle(jets, i_jet)
dx = 0

View File

@@ -1,7 +1,4 @@
# -*- encoding: utf-8 -*-
import sys
from typing import FrozenSet
import numpy as np

View File

@@ -1,11 +1,9 @@
# -*- encoding: utf-8 -*-
import sys
from typing import Literal
from typing import Any, Literal
import numpy as np
import parse
from tqdm import tqdm
import parse # pyright: ignore[reportMissingTypeStubs]
from numpy.typing import NDArray
Reagent = Literal["ore", "clay", "obsidian", "geode"]
REAGENTS: tuple[Reagent, ...] = (
@@ -37,7 +35,7 @@ class State:
self.robots = robots
self.reagents = reagents
def __eq__(self, other) -> bool:
def __eq__(self, other: object) -> bool:
return (
isinstance(other, State)
and self.robots == other.robots
@@ -68,7 +66,7 @@ lines = sys.stdin.read().splitlines()
blueprints: list[dict[Reagent, IntOfReagent]] = []
for line in lines:
r = parse.parse(
r: list[int] = parse.parse( # type: ignore
"Blueprint {}: "
"Each ore robot costs {:d} ore. "
"Each clay robot costs {:d} ore. "
@@ -88,7 +86,6 @@ for line in lines:
def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
# since we can only build one robot per time, we do not need more than X robots
# of type K where X is the maximum number of K required among all robots, e.g.,
# in the first toy blueprint, we need at most 4 ore robots, 14 clay ones and 7
@@ -97,12 +94,12 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
name: max(blueprint[r].get(name, 0) for r in REAGENTS) for name in REAGENTS
}
state_after_t: dict[int, set[State]] = {0: [State()]}
state_after_t: dict[int, set[State]] = {0: {State()}}
for t in range(1, max_time + 1):
# list of new states at the end of step t that we are going to prune later
states_for_t: set[State] = set()
robots_that_can_be_built: list[Reagent]
for state in state_after_t[t - 1]:
robots_that_can_be_built = [
@@ -136,7 +133,7 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
for robot in robots_that_can_be_built:
robots = state.robots.copy()
robots[robot] += 1
reagents = {
reagents: IntOfReagent = {
reagent: state.reagents[reagent]
+ state.robots[reagent]
- blueprint[robot].get(reagent, 0)
@@ -155,7 +152,7 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
]
)
to_keep = []
to_keep: list[NDArray[np.integer[Any]]] = []
while len(np_states) > 0:
first_dom = (np_states[1:] >= np_states[0]).all(axis=1).any()

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import sys
@@ -49,7 +47,7 @@ lines = sys.stdin.readlines()
values = [(ord(row[0]) - ord("A"), ord(row[2]) - ord("X")) for row in lines]
# part 1 - 13526
print(f"score 1 is {sum(score_1(*v) for v in values)}")
print(f"answer 1 is {sum(score_1(*v) for v in values)}")
# part 2 - 14204
print(f"score 2 is {sum(score_2(*v) for v in values)}")
print(f"answer 2 is {sum(score_2(*v) for v in values)}")

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
from __future__ import annotations
import sys
@@ -21,7 +19,6 @@ class Number:
def decrypt(numbers: list[Number], key: int, rounds: int) -> int:
numbers = numbers.copy()
original = numbers.copy()

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import operator
import sys
from typing import Callable

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import re
import sys
from typing import Callable
@@ -126,7 +124,6 @@ def wrap_part_2(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
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"

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import itertools
import sys
from collections import defaultdict
@@ -41,7 +39,7 @@ def round(
directions: Directions,
):
to_move: dict[tuple[int, int], list[tuple[int, int]]] = defaultdict(lambda: [])
for (y, x) in positions:
for y, x in positions:
elves = {
(dy, dx): (y + dy, x + dx) in positions
for dy, dx in itertools.product((-1, 0, 1), (-1, 0, 1))

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import heapq
import math
import sys

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import sys
lines = sys.stdin.read().splitlines()

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import string
import sys
@@ -13,7 +11,7 @@ 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}")
print(f"answer 1 is {part1}")
# part 2
n_per_group = 3
@@ -22,4 +20,4 @@ part2 = sum(
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}")
print(f"answer 2 is {part2}")

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import sys
lines = [line.strip() for line in sys.stdin.readlines()]
@@ -12,8 +10,8 @@ def make_range(value: str) -> set[int]:
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}")
answer_1 = sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections)
print(f"answer 1 is {answer_1}")
score_2 = sum(bool(s1.intersection(s2)) for s1, s2 in sections)
print(f"score 1 is {score_2}")
answer_2 = sum(bool(s1.intersection(s2)) for s1, s2 in sections)
print(f"answer 1 is {answer_2}")

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import copy
import sys

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import sys

View File

@@ -1,5 +1,3 @@
# -*- encoding: utf-8 -*-
import sys
from pathlib import Path

View File

@@ -1,8 +1,7 @@
# -*- encoding: utf-8 -*-
import sys
import numpy as np
from numpy.typing import NDArray
lines = sys.stdin.read().splitlines()
@@ -27,7 +26,7 @@ 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:
def viewing_distance(row_of_trees: NDArray[np.int_], value: int) -> int:
w = np.where(row_of_trees >= value)[0]
if not w.size:

View File

@@ -1,12 +1,9 @@
# -*- encoding: utf-8 -*-
import sys
import numpy as np
def move(head: tuple[int, int], command: str) -> tuple[int, int]:
h_col, h_row = head
if command == "L":
@@ -22,7 +19,6 @@ def move(head: tuple[int, int], command: str) -> tuple[int, int]:
def follow(head: tuple[int, int], tail: tuple[int, int]) -> tuple[int, int]:
h_col, h_row = head
t_col, t_row = tail
@@ -33,8 +29,7 @@ def follow(head: tuple[int, int], tail: tuple[int, int]) -> tuple[int, int]:
def run(commands: list[str], n_blocks: int) -> list[tuple[int, int]]:
blocks = [(0, 0) for _ in range(n_blocks)]
blocks: list[tuple[int, int]] = [(0, 0) for _ in range(n_blocks)]
visited = [blocks[-1]]
for command in commands:

View File

@@ -0,0 +1,49 @@
from typing import Any, Iterator
from ..base import BaseSolver
def find_values(lines: list[str], 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
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))

View File

@@ -0,0 +1,100 @@
from typing import Any, Iterator, Literal, cast
from ..base import BaseSolver
Symbol = Literal["|", "-", "L", "J", "7", "F", ".", "S"]
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines: list[list[Symbol]] = [
[cast(Symbol, symbol) for symbol in line] for line in 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 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]
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 (i, j) == (si, sj):
break
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)

View File

@@ -0,0 +1,42 @@
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()
data = np.array([[c == "#" for c in line] for line in lines])
rows = {c for c in range(data.shape[0]) if not data[c, :].any()}
columns = {c for c in range(data.shape[1]) if not data[:, c].any()}
galaxies_y, galaxies_x = np.where(data) # type: ignore
def compute_total_distance(expansion: int) -> int:
distances: list[int] = []
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])
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)

View File

@@ -0,0 +1,103 @@
from functools import lru_cache
from typing import Any, Iterable, Iterator
from ..base import BaseSolver
@lru_cache
def compute_fitting_arrangements(pattern: str, counts: tuple[int, ...]) -> int:
"""
fn3p tries to fit ALL values in counts() inside the pattern.
"""
# no pattern -> ok if nothing to fit, otherwise ko
if not pattern:
count = 1 if not counts else 0
# no count -> ok if pattern has no mandatory entry, else ko
elif not counts:
count = 1 if pattern.find("#") == -1 else 0
# cannot fit all values -> ko
elif len(pattern) < sum(counts) + len(counts) - 1:
count = 0
elif len(pattern) < counts[0]:
count = 0
else:
count = 0
if pattern[0] == "?":
count += compute_fitting_arrangements(pattern[1:], counts)
if len(pattern) == counts[0]:
count += 1
elif pattern[counts[0]] != "#":
count += compute_fitting_arrangements(pattern[counts[0] + 1 :], counts[1:])
return count
@lru_cache
def compute_possible_arrangements(
patterns: tuple[str, ...], counts: tuple[int, ...]
) -> int:
if not patterns:
return 1 if not counts else 0
with_hash = sum(1 for p in patterns[1:] if p.find("#") >= 0)
if with_hash > len(counts):
return 0
to_fit = counts if with_hash == 0 else counts[:-with_hash]
remaining = () if with_hash == 0 else counts[-with_hash:]
if not to_fit:
if patterns[0].find("#") != -1:
return 0
return compute_possible_arrangements(patterns[1:], remaining)
elif patterns[0].find("#") != -1 and len(patterns[0]) < to_fit[0]:
return 0
elif patterns[0].find("?") == -1:
if len(patterns[0]) != to_fit[0]:
return 0
return compute_possible_arrangements(patterns[1:], counts[1:])
else:
return sum(
fp * compute_possible_arrangements(patterns[1:], to_fit[i:] + remaining)
for i in range(len(to_fit) + 1)
if (fp := compute_fitting_arrangements(patterns[0], to_fit[:i])) > 0
)
class Solver(BaseSolver):
def compute_all_possible_arrangements(
self, 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,
)
return count
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
# part 1
yield self.compute_all_possible_arrangements(lines, 1)
# part 2
yield self.compute_all_possible_arrangements(lines, 5)

View File

@@ -0,0 +1,43 @@
from typing import Any, Callable, Iterator, Literal
from ..base import BaseSolver
def split(block: list[str], axis: Literal[0, 1], count: int) -> int:
n_iter = len(block) if axis == 0 else len(block[0])
n_check = len(block) if axis == 1 else len(block[0])
at: Callable[[int, int], str] = (
(lambda i, j: block[i][j]) if axis == 0 else (lambda i, j: block[j][i])
)
for i in range(n_iter - 1):
size = min(i + 1, n_iter - i - 1)
if (
sum(
at(i - s, j) != at(i + 1 + s, j)
for s in range(0, size)
for j in range(n_check)
)
== count
):
return i + 1
return 0
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
blocks = [block.splitlines() for block in input.split("\n\n")]
# part 1
yield sum(
split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0)
for block in blocks
)
# part 2
yield sum(
split(block, axis=1, count=1) + 100 * split(block, axis=0, count=1)
for block in blocks
)

View File

@@ -0,0 +1,70 @@
from typing import Any, Iterator, TypeAlias
from ..base import BaseSolver
RockGrid: TypeAlias = list[list[str]]
def slide_rocks_top(rocks: RockGrid) -> RockGrid:
top = [0 if c == "." else 1 for c in rocks[0]]
for row in range(1, len(rocks)):
for col in range(len(rocks[0])):
match rocks[row][col]:
case "O":
if top[col] != row:
rocks[top[col]][col] = "O"
rocks[row][col] = "."
top[col] = top[col] + 1
case "#":
top[col] = row + 1
case _:
pass
return rocks
def cycle(rocks: RockGrid) -> RockGrid:
for _ in range(4):
rocks = slide_rocks_top(rocks)
rocks = [
[rocks[len(rocks) - j - 1][i] for j in range(len(rocks))]
for i in range(len(rocks[0]))
]
return rocks
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
rocks0 = [list(line) for line in input.splitlines()]
rocks = slide_rocks_top([[c for c in r] for r in rocks0])
# part 1
yield sum(
(len(rocks) - i) * sum(1 for c in row if c == "O")
for i, row in enumerate(rocks)
)
# part 2
rocks = rocks0
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
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
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])
)

View File

@@ -0,0 +1,33 @@
from functools import reduce
from typing import Any, Iterator
from ..base import BaseSolver
def _hash(s: str) -> int:
return reduce(lambda v, u: ((v + ord(u)) * 17) % 256, s, 0)
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
steps = input.split(",")
# part 1
yield sum(map(_hash, steps))
# 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)
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)
)

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