Compare commits
14 Commits
00000120e2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
0000026012
|
|||
|
0000025016
|
|||
|
00000240ea
|
|||
|
000002303f
|
|||
|
000002201f
|
|||
|
00000210a8
|
|||
|
000002007f
|
|||
|
00000190d0
|
|||
|
000001806d
|
|||
|
0000017088
|
|||
|
00000160c2
|
|||
|
000001505b
|
|||
|
000001407e
|
|||
|
000001301f
|
@@ -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
2
data/example/2019/03.txt
Normal 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
1
data/example/2019/04.txt
Normal file
@@ -0,0 +1 @@
|
||||
229228-249228
|
||||
4
data/example/2025/06.txt
Normal file
4
data/example/2025/06.txt
Normal 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
16
data/example/2025/07.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
.......S.......
|
||||
...............
|
||||
.......^.......
|
||||
...............
|
||||
......^.^......
|
||||
...............
|
||||
.....^.^.^.....
|
||||
...............
|
||||
....^.^...^....
|
||||
...............
|
||||
...^.^...^.^...
|
||||
...............
|
||||
..^...^.....^..
|
||||
...............
|
||||
.^.^.^.^.^...^.
|
||||
...............
|
||||
20
data/example/2025/08.txt
Normal file
20
data/example/2025/08.txt
Normal 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
8
data/example/2025/09.txt
Normal 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
3
data/example/2025/10.txt
Normal 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
15
data/example/2025/11.txt
Normal 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
0
data/example/2025/12.txt
Normal file
@@ -1,2 +1,7 @@
|
||||
# cli
|
||||
python-dotenv==1.2.1
|
||||
requests==2.32.5
|
||||
|
||||
# solutions
|
||||
numpy==2.3.5
|
||||
scipy==1.16.3
|
||||
|
||||
52
src/solution/year_2019/day_03.py
Normal file
52
src/solution/year_2019/day_03.py
Normal 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)
|
||||
66
src/solution/year_2019/day_04.py
Normal file
66
src/solution/year_2019/day_04.py
Normal 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
|
||||
52
src/solution/year_2025/day_06.py
Normal file
52
src/solution/year_2025/day_06.py
Normal 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
|
||||
56
src/solution/year_2025/day_07.py
Normal file
56
src/solution/year_2025/day_07.py
Normal 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
|
||||
135
src/solution/year_2025/day_08.py
Normal file
135
src/solution/year_2025/day_08.py
Normal 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
|
||||
156
src/solution/year_2025/day_09.py
Normal file
156
src/solution/year_2025/day_09.py
Normal 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
|
||||
98
src/solution/year_2025/day_10.py
Normal file
98
src/solution/year_2025/day_10.py
Normal 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
|
||||
86
src/solution/year_2025/day_11.py
Normal file
86
src/solution/year_2025/day_11.py
Normal 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
|
||||
65
src/solution/year_2025/day_12.py
Normal file
65
src/solution/year_2025/day_12.py
Normal 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
|
||||
Reference in New Issue
Block a user