Compare commits

..

7 Commits

13 changed files with 434 additions and 47 deletions

View File

@@ -113,11 +113,11 @@ def part_2(input_data: str) -> Any:
return None
def test_part_1(example_data)
def test_part_1(example_data):
assert part_1(example_data) == 0
def test_part_2(example_data)
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

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

@@ -1,4 +1,3 @@
from collections.abc import Generator
from typing import Any
Point = tuple[int, int]
@@ -14,10 +13,7 @@ def _parse_input(input_data) -> list[Point]:
def _rectangle_size(p1: Point, p2: Point) -> int:
x1, y1 = p1
x2, y2 = p2
return (abs(x1 - x2) + 1) * (abs(y1 - y2) + 1)
return (abs(p1[0] - p2[0]) + 1) * (abs(p1[1] - p2[1]) + 1)
def part_1(input_data: str) -> Any:
@@ -47,22 +43,8 @@ def _get_dir(p1: Point, p2: Point) -> Point:
return dx, dy
def _get_segment(p1: Point, p2: Point) -> Generator[Point]:
dx, dy = _get_dir(p1, p2)
points = []
sx, sy = p1
while (sx, sy) != p2:
yield sx, sy
points.append((sx, sy))
sx, sy = sx + dx, sy + dy
yield p2
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}))}
return {v: k for k, v in enumerate(sorted({p[y] for p in points}), 1)}
def _find_inner(
@@ -80,15 +62,17 @@ def _find_inner(
c1, c2 = c(p1), c(p2)
rng = _get_segment(c1, c2)
x, y = c1
dx, dy = _get_dir(c1, c2)
# rotate 90 positive as inside is in the positive direction
dx, dy = -dy, dx
pdx, pdy = -dy, dx
for x, y in rng:
while (x, y) != c2:
inner.add((x, y))
inside.add((x + dx, y + dy))
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)
@@ -106,18 +90,6 @@ def _find_inner(
return inner
def _get_corner_points(p1: Point, p2: Point) -> tuple[Point, ...]:
if p1 == p2:
return (p1,)
px1, py1 = p1
px2, py2 = p2
p3 = px1, py2
p4 = px2, py1
return p1, p3, p2, p4
def part_2(input_data: str) -> Any:
points = _parse_input(input_data)
@@ -129,6 +101,19 @@ def part_2(input_data: str) -> Any:
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):
@@ -136,19 +121,28 @@ def part_2(input_data: str) -> Any:
for p2 in points[idx + 1 :]:
c2 = c(p2)
c1, c2, c3, c4 = _get_corner_points(c1, c2)
size = _rectangle_size(p1, p2)
if m and size <= m:
continue
is_inside = (
all(x in inner for x in _get_segment(c1, c2))
and all(x in inner for x in _get_segment(c2, c3))
and all(x in inner for x in _get_segment(c3, c4))
and all(x in inner for x in _get_segment(c4, c1))
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
size = _rectangle_size(p1, p2)
m = max(m or size, size)
return m

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