Compare commits

...

14 Commits

20 changed files with 862 additions and 9 deletions

View File

@@ -1,3 +1,4 @@
import datetime
import pathlib
import requests
@@ -15,8 +16,21 @@ def download(year: int, day: int, session_token: str | None) -> bytes:
response = requests.get(url, cookies=cookies)
if response.status_code != 200:
status = response.status_code
# https://stackoverflow.com/a/39079819/11286805
local_tz = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
now = datetime.datetime.now(local_tz)
release = datetime.datetime(year, 12, day, 5, tzinfo=datetime.UTC)
if now < release:
raise exceptions.AocError(
f"failed to fetch input file {year}/{day:02} (http:{response.status_code})"
f"unreleased input file {year}/{day:02} (http:{status})"
)
raise exceptions.AocError(
f"failed to fetch input file {year}/{day:02} (http:{status})"
" - is AOC_SESSION_TOKEN environment variable set?"
)
@@ -54,7 +68,7 @@ def get(
path.parent.mkdir(parents=True, exist_ok=True)
with open(path) as f:
return f.read().strip()
return f.read().rstrip("\n")
def get_or_download(
@@ -92,20 +106,19 @@ from typing import Any
def part_1(input_data: str) -> Any:
# your part 1 solution here
return None
def part_2(input_data: str) -> Any:
# your part 2 solution here
return None
# alternatively you can implement a "two in one solution" like this
# part_1 and part_2 must be removed or commented out in this case
# if tuple or list is returned elements will be printed on separate lines
# def part_1_2(input_data) -> Any:
# return None
def test_part_1(example_data):
assert part_1(example_data) == 0
def test_part_2(example_data):
assert part_2(example_data) == 0
"""

2
data/example/2019/03.txt Normal file
View File

@@ -0,0 +1,2 @@
R75,D30,R83,U83,L12,D49,R71,U7,L72
U62,R66,U55,R34,D71,R55,D58,R83

1
data/example/2019/04.txt Normal file
View File

@@ -0,0 +1 @@
229228-249228

4
data/example/2025/06.txt Normal file
View File

@@ -0,0 +1,4 @@
123 328 51 64
45 64 387 23
6 98 215 314
* + * +

16
data/example/2025/07.txt Normal file
View File

@@ -0,0 +1,16 @@
.......S.......
...............
.......^.......
...............
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
...............

20
data/example/2025/08.txt Normal file
View File

@@ -0,0 +1,20 @@
162,817,812
57,618,57
906,360,560
592,479,940
352,342,300
466,668,158
542,29,236
431,825,988
739,650,466
52,470,668
216,146,977
819,987,18
117,168,530
805,96,715
346,949,466
970,615,88
941,993,340
862,61,35
984,92,344
425,690,689

8
data/example/2025/09.txt Normal file
View File

@@ -0,0 +1,8 @@
7,1
11,1
11,7
9,7
9,5
2,5
2,3
7,3

3
data/example/2025/10.txt Normal file
View File

@@ -0,0 +1,3 @@
[.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
[...#.] (0,2,3,4) (2,3) (0,4) (0,1,2) (1,2,3,4) {7,5,12,7,2}
[.###.#] (0,1,2,3,4) (0,3,4) (0,1,2,4,5) (1,2) {10,11,11,5,10,5}

15
data/example/2025/11.txt Normal file
View File

@@ -0,0 +1,15 @@
aaa: you hhh
you: bbb ccc
bbb: ddd eee
ccc: ddd eee fff
ddd: ggg
eee: out
fff: out
ggg: out
hhh: ccc fff iii
iii: out
svr: zzz www
zzz: dac
dac: www out fft
fft: out
www: out

0
data/example/2025/12.txt Normal file
View File

View File

@@ -1,2 +1,7 @@
# cli
python-dotenv==1.2.1
requests==2.32.5
# solutions
numpy==2.3.5
scipy==1.16.3

View File

@@ -0,0 +1,52 @@
import collections
from typing import Any
DIR_MAP = {
"U": (0, -1),
"D": (0, 1),
"L": (-1, 0),
"R": (1, 0),
}
def _parse_input(input_data: str) -> list[list[tuple[str, int]]]:
wires = []
for line in input_data.splitlines():
wire = [(w[0], int(w[1:])) for w in line.split(",")]
wires.append(wire)
return wires
def part_1_2(input_data: str) -> Any:
wires = _parse_input(input_data)
locs = collections.defaultdict(dict)
m1 = None
m2 = None
for wid, wire in enumerate(wires):
x, y, w = 0, 0, 0
for direction, distance in wire:
dx, dy = DIR_MAP[direction]
for _ in range(distance):
w += 1
x += dx
y += dy
loc = locs[(x, y)]
loc.setdefault(wid, w)
if len(loc) == 2:
d1 = abs(x) + abs(y)
m1 = min(m1 or d1, d1)
d2 = loc[0] + loc[1]
m2 = min(m2 or d2, d2)
return m1, m2
def test_part_1_2(example_data):
assert part_1_2(example_data) == (159, 610)

View File

@@ -0,0 +1,66 @@
from typing import Any
def _test_len(s: str, n: int):
return len(s) == n
def _test_increasing(s: str):
return all(s[i - 1] <= s[i] for i in range(1, len(s)))
def _test_adjacent(s: str):
return any(s[i - 1] == s[i] for i in range(1, len(s)))
def _get_index(s: str, i: int) -> str | None:
if not (0 <= i < len(s)):
return None
return s[i]
def _test_adjacent_2(s: str) -> bool:
for i in range(1, len(s)):
s1 = s[i]
s2 = s[i - 1]
s0 = _get_index(s, i - 2)
s3 = _get_index(s, i + 1)
if s1 == s2 and s1 != s0 and s2 != s3:
return True
return False
def _is_valid_password(n: int) -> bool:
s = str(n)
return all([_test_len(s, 6), _test_adjacent(s), _test_increasing(s)])
def _is_valid_password_2(n: int) -> bool:
s = str(n)
return all([_test_len(s, 6), _test_adjacent_2(s), _test_increasing(s)])
def part_1(input_data: str) -> Any:
a, b = input_data.split("-")
a, b = int(a), int(b)
return len([x for x in range(a, b + 1) if _is_valid_password(x)])
def part_2(input_data: str) -> Any:
a, b = input_data.split("-")
a, b = int(a), int(b)
return len([x for x in range(a, b + 1) if _is_valid_password_2(x)])
def test_part_1(example_data):
assert part_1(example_data) == 316
def test_part_2(example_data):
assert part_2(example_data) == 218

View File

@@ -0,0 +1,52 @@
import functools
import math
from typing import Any
def part_1(input_data: str) -> Any:
lines = input_data.splitlines()
numbers = [list(map(int, line.split())) for line in lines[:-1]]
signs = lines[-1].split()
c = 0
for i, x in enumerate(signs):
acc = [n[i] for n in numbers]
c += sum(acc) if x == "+" else math.prod(acc)
return c
def part_2(input_data: str) -> Any:
lines = input_data.splitlines()
numbers = [[int(s) if s != " " else None for s in line] for line in lines[:-1]]
signs = [s if s != " " else None for s in lines[-1]]
idx = len(signs) - 1
c = 0
while idx >= 0:
acc = []
while True:
_digits = [n[idx] for n in numbers]
digits = [n for n in _digits if n is not None]
acc.append(functools.reduce(lambda acc, x: acc * 10 + x, digits))
sign = signs[idx]
if sign is not None:
c += sum(acc) if sign == "+" else math.prod(acc)
idx -= 2
break
idx -= 1
return c
def test_part_1(example_data):
assert part_1(example_data) == 4277556
def test_part_2(example_data):
assert part_2(example_data) == 3263827

View File

@@ -0,0 +1,56 @@
from functools import lru_cache
from typing import Any
def part_1(input_data: str) -> Any:
lines = input_data.splitlines()
first, rest = lines[0], lines[1:]
s = {i for i, x in enumerate(first) if x == "S"}
ns: set[int] = set()
c = 0
for line in rest:
ns.clear()
while s:
n = s.pop()
if line[n] == "^":
ns.add(n - 1)
ns.add(n + 1)
c += 1
else:
ns.add(n)
s.update(ns)
return c
def trace_path(n: int, i: int, lines: list[str]) -> int:
@lru_cache(None)
def aux(n: int, i: int) -> int:
for idx, line in filter(lambda x: x[0] >= i, enumerate(lines)):
if line[n] == "^":
left = aux(n - 1, idx + 1)
right = aux(n + 1, idx + 1)
return 1 + left + right
return 0
return aux(n, i)
def part_2(input_data: str) -> Any:
lines = input_data.splitlines()
first, rest = lines[0], lines[1:]
n = next(filter(lambda x: x[1] == "S", enumerate(first)))[0]
return 1 + trace_path(n, 0, rest)
def test_part_1(example_data):
assert part_1(example_data) == 21
def test_part_2(example_data):
assert part_2(example_data) == 40

View File

@@ -0,0 +1,135 @@
import math
from collections.abc import Iterable
from typing import Any, Self
Point = tuple[int, int, int]
def _parse_input(input_data: str) -> list[Point]:
points = []
for line in input_data.splitlines():
a, b, c, *_ = list(map(int, line.split(",")))
points.append((a, b, c))
return points
def _get_sorted_pairs(points: list[Point]) -> list[tuple[int, Point, Point]]:
pairs: list[tuple[int, Point, Point]] = []
for idx, p1 in enumerate(points):
x1, y1, z1 = p1
for p2 in points[idx + 1 :]:
x2, y2, z2 = p2
dist = (x1 - x2) ** 2 + (y1 - y2) ** 2 + (z1 - z2) ** 2
pairs.append((dist, p1, p2))
pairs.sort()
return pairs
class UnionFind:
def __init__(self):
# internal counter for ids
self._component_id = 0
# component id to size mapper
self.component_size: dict[int, int] = {}
# point to id mapper
self.points: dict[Point, int] = {}
# point to parent point mapper
self.parent: dict[Point, Point] = {}
@classmethod
def from_points(cls, points: Iterable[Point]) -> Self:
c = cls()
for p in points:
c.insert_point(p)
return c
def _issue_component_id(self) -> int:
self._component_id += 1
return self._component_id
def _get_parent(self, p: Point) -> Point:
if self.parent[p] == p:
return p
parent = self._get_parent(self.parent[p])
self.parent[p] = parent
self.points[p] = self.points[parent]
return parent
def _update_parent(self, point: Point, parent: Point):
ppoint = self._get_parent(point)
pparent = self._get_parent(parent)
self.parent[ppoint] = pparent
self.points[pparent] = self.points[pparent]
def insert_point(self, p: Point) -> None:
pid = self._issue_component_id()
assert p not in self.points
self.points[p] = pid
self.parent[p] = p
self.component_size[pid] = 1
def connect(self, p1: Point, p2: Point):
if p1 not in self.points:
self.insert_point(p1)
if p2 not in self.points:
self.insert_point(p2)
pid1 = self.points[self._get_parent(p1)]
pid2 = self.points[self._get_parent(p2)]
if pid1 == pid2:
return
# swap components so we can assume component pid1 is larger
if self.component_size[pid1] < self.component_size[pid2]:
p1, p2 = p2, p1
pid1, pid2 = pid2, pid1
self.component_size[pid1] += self.component_size[pid2]
self.component_size.pop(pid2)
self._update_parent(p2, p1)
def part_1(input_data: str) -> Any:
points = _parse_input(input_data)
pairs = _get_sorted_pairs(points)
g = UnionFind.from_points(points)
for _, p1, p2 in pairs[:1000]:
g.connect(p1, p2)
return math.prod(sorted(g.component_size.values())[-3:])
def part_2(input_data: str) -> Any:
points = _parse_input(input_data)
pairs = _get_sorted_pairs(points)
g = UnionFind.from_points(points)
for _, p1, p2 in pairs:
g.connect(p1, p2)
if len(g.component_size) == 1:
return p1[0] * p2[0]
return None
def test_part_1(example_data):
assert part_1(example_data) == 20
def test_part_2(example_data):
assert part_2(example_data) == 25272

View File

@@ -0,0 +1,156 @@
from typing import Any
Point = tuple[int, int]
def _parse_input(input_data) -> list[Point]:
points = []
for line in input_data.splitlines():
a, b = line.split(",")
points.append((int(a), int(b)))
return points
def _rectangle_size(p1: Point, p2: Point) -> int:
return (abs(p1[0] - p2[0]) + 1) * (abs(p1[1] - p2[1]) + 1)
def part_1(input_data: str) -> Any:
lines = _parse_input(input_data)
m = None
for idx, p1 in enumerate(lines):
for p2 in lines[idx + 1 :]:
size = _rectangle_size(p1, p2)
m = max(size, m or size)
return m
def _get_dir(p1: Point, p2: Point) -> Point:
x1, y1 = p1
x2, y2 = p2
dx = x2 - x1
dy = y2 - y1
if dx:
dx = dx // abs(dx)
if dy:
dy = dy // abs(dy)
return dx, dy
def _compress_coordinates(points: list[Point], y: bool) -> dict[int, int]:
return {v: k for k, v in enumerate(sorted({p[y] for p in points}), 1)}
def _find_inner(
points: list[Point], xc: dict[int, int], yc: dict[int, int]
) -> set[Point]:
def c(p: Point) -> Point:
return xc[p[0]], yc[p[1]]
inner = set()
inside = set()
for idx in range(len(points)):
p1 = points[idx - 1]
p2 = points[idx]
c1, c2 = c(p1), c(p2)
x, y = c1
dx, dy = _get_dir(c1, c2)
pdx, pdy = -dy, dx
while (x, y) != c2:
inner.add((x, y))
inside.add((x + pdx, y + pdy))
x, y = x + dx, y + dy
inner.add((x, y))
inside.add((x + pdx, y + pdy))
stack = list(inside - inner)
dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
while stack:
px, py = stack.pop()
inner.add((px, py))
for dx, dy in dirs:
nx, ny = px + dx, py + dy
if (nx, ny) in inner:
continue
stack.append((nx, ny))
return inner
def part_2(input_data: str) -> Any:
points = _parse_input(input_data)
x_compression = _compress_coordinates(points, False)
y_compression = _compress_coordinates(points, True)
def c(p: Point) -> Point:
return x_compression[p[0]], y_compression[p[1]]
inner = _find_inner(points, x_compression, y_compression)
w = max(x_compression.values())
h = max(y_compression.values())
prefix = [[0] * (h + 1) for _ in range(w + 1)]
for x in range(1, w + 1):
for y in range(1, h + 1):
prefix[x][y] = (
((x, y) in inner)
+ prefix[x - 1][y]
+ prefix[x][y - 1]
- prefix[x - 1][y - 1]
)
m = None
for idx, p1 in enumerate(points):
c1 = c(p1)
for p2 in points[idx + 1 :]:
c2 = c(p2)
size = _rectangle_size(p1, p2)
if m and size <= m:
continue
min_x = min(c1[0], c2[0])
max_x = max(c1[0], c2[0])
min_y = min(c1[1], c2[1])
max_y = max(c1[1], c2[1])
c_expected_size = _rectangle_size(c1, c2)
c_size = (
prefix[max_x][max_y]
- prefix[min_x - 1][max_y]
- prefix[max_x][min_y - 1]
+ prefix[min_x - 1][min_y - 1]
)
is_inside = c_size == c_expected_size
if not is_inside:
continue
m = max(m or size, size)
return m
def test_part_1(example_data):
assert part_1(example_data) == 50
def test_part_2(example_data):
assert part_2(example_data) == 24

View File

@@ -0,0 +1,98 @@
import collections
import re
from typing import Any
import numpy as np
from scipy import optimize
def _parse_input(input_data: str):
res: list[tuple[int, list[list[int]], list[int]]] = []
for line in input_data.splitlines():
_state = re.findall(r"\[([.#]*)\]", line)[0]
state = sum(1 << i for i, x in enumerate(_state) if x == "#")
_buttons = re.findall(r"\(([0-9,]*)\)", line)
buttons = [[int(n) for n in m.split(",")] for m in _buttons]
_joltage = re.findall(r" {([0-9,]*)}$", line)[0].strip()
joltage = list(map(int, _joltage.split(",")))
res.append((state, buttons, joltage))
return res
def _bfs(end: int, neighs: list[int]) -> int:
start = 0
dq = collections.deque([(0, start)])
visited = {0}
while dq:
d, p = dq.popleft()
if p == end:
return d
for n in neighs:
np = p ^ n
if np in visited:
continue
visited.add(np)
dq.append((d + 1, np))
raise ValueError(f"unable to calibrate {end}")
def _milp(vecs: list[list[int]], goal: list[int]) -> int:
n = len(goal)
m = len(vecs)
a = np.zeros((n, m), dtype=int)
for j, vec in enumerate(vecs):
for i in vec:
a[i][j] = 1
c = np.ones(m)
b = np.array(goal)
constraints = optimize.LinearConstraint(a, b, b) # type: ignore
bounds = optimize.Bounds(0, np.inf)
integrality = np.ones(m)
result = optimize.milp(
c,
constraints=constraints,
bounds=bounds,
integrality=integrality,
)
return int(result.fun)
def part_1(input_data: str) -> Any:
parsed = _parse_input(input_data)
s = 0
for end, switches, _ in parsed:
neighs = [sum(1 << n for n in m) for m in switches]
s += _bfs(end, neighs)
return s
def part_2(input_data: str) -> Any:
return sum(_milp(vecs, goal) for _, vecs, goal in _parse_input(input_data))
def test_part_1(example_data):
assert part_1(example_data) == 7
def test_part_2(example_data):
assert part_2(example_data) == 33

View File

@@ -0,0 +1,86 @@
import collections
import functools
from typing import Any
def _parse_input(input_data: str) -> tuple[dict[str, int], dict[int, list[int]]]:
counter = 0
ids = {}
graph = collections.defaultdict(list)
for line in input_data.splitlines():
node, _neighs = line.split(": ")
neighs = _neighs.split()
if node not in ids:
ids[node] = counter
counter += 1
for n in filter(lambda x: x not in ids, neighs):
ids[n] = counter
counter += 1
for n in neighs:
graph[ids[n]].append(ids[node])
return ids, graph
def _count_paths(
graph: dict[int, list[int]],
start: int,
end: int,
skip: set[int] | None = None,
) -> int:
if skip is None:
skip = set()
@functools.cache
def traverse(node: int):
if node in skip:
return 0
if node == start:
return 1
return sum(traverse(n) for n in graph[node])
return traverse(end)
def part_1(input_data: str) -> Any:
ids, graph = _parse_input(input_data)
print(graph)
you = ids["you"]
out = ids["out"]
return _count_paths(graph, you, out)
def part_2(input_data: str) -> Any:
ids, graph = _parse_input(input_data)
svr = ids["svr"]
out = ids["out"]
dac = ids["dac"]
fft = ids["fft"]
svr_dac = _count_paths(graph, svr, dac, {out, fft})
svr_fft = _count_paths(graph, svr, fft, {out, dac})
dac_fft = _count_paths(graph, dac, fft, {out})
fft_dac = _count_paths(graph, fft, dac, {out})
dac_out = _count_paths(graph, dac, out, {fft})
fft_out = _count_paths(graph, fft, out, {dac})
return svr_dac * dac_fft * fft_out + svr_fft * fft_dac * dac_out
def test_part_1(example_data):
assert part_1(example_data) == 5
def test_part_2(example_data):
assert part_2(example_data) == 1

View File

@@ -0,0 +1,65 @@
import functools
from typing import Any
class Shape:
def __init__(self, shape: list[str]):
self.shape = shape
@functools.cached_property
def size(self) -> int:
return sum(sum(x == "#" for x in s) for s in self.shape)
class Grid:
def __init__(self, a: int, b: int, amount: list[int]) -> None:
self.a = a
self.b = b
self.amount = amount
@functools.cached_property
def size(self) -> int:
return self.a * self.b
def _parse_input(input_data: str) -> tuple[list[Shape], list[Grid]]:
*_shapes, _grids = input_data.split("\n\n")
shapes = []
for s in _shapes:
shapes.append(Shape(s.splitlines()[1:]))
grids = []
for g in _grids.splitlines():
size, amount = g.split(": ")
a, b = size.split("x")
grids.append(Grid(int(a), int(b), list(map(int, amount.split()))))
return shapes, grids
def part_1(input_data: str) -> Any:
shapes, grids = _parse_input(input_data)
s = 0
for g in grids:
gifts = sum(a * b.size for a, b in zip(g.amount, shapes, strict=True))
# all cases are trivial xd
if gifts <= g.size:
s += 1
return s
def part_2(_: str) -> Any:
return "Marry Christmas!"
def test_part_1(example_data):
assert part_1(example_data) == 0
def test_part_2(example_data):
assert part_2(example_data) == 0