Compare commits
21 Commits
00000050d9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
0000026012
|
|||
|
0000025016
|
|||
|
00000240ea
|
|||
|
000002303f
|
|||
|
000002201f
|
|||
|
00000210a8
|
|||
|
000002007f
|
|||
|
00000190d0
|
|||
|
000001806d
|
|||
|
0000017088
|
|||
|
00000160c2
|
|||
|
000001505b
|
|||
|
000001407e
|
|||
|
000001301f
|
|||
|
00000120e2
|
|||
|
0000011054
|
|||
|
00000100ef
|
|||
|
00000090e4
|
|||
|
00000080ac
|
|||
|
0000007071
|
|||
|
00000060e4
|
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# lint & format
|
||||||
|
0000007071a01bec5b648d00bbc2055eef6f59e0
|
||||||
@@ -2,6 +2,6 @@ repos:
|
|||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.14.7
|
rev: v0.14.7
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff-check
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
args: [ --check ]
|
args: [ --check ]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import datetime
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -15,8 +16,21 @@ def download(year: int, day: int, session_token: str | None) -> bytes:
|
|||||||
response = requests.get(url, cookies=cookies)
|
response = requests.get(url, cookies=cookies)
|
||||||
|
|
||||||
if response.status_code != 200:
|
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(
|
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?"
|
" - is AOC_SESSION_TOKEN environment variable set?"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,7 +68,7 @@ def get(
|
|||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
return f.read().strip()
|
return f.read().rstrip("\n")
|
||||||
|
|
||||||
|
|
||||||
def get_or_download(
|
def get_or_download(
|
||||||
@@ -92,20 +106,19 @@ from typing import Any
|
|||||||
|
|
||||||
|
|
||||||
def part_1(input_data: str) -> Any:
|
def part_1(input_data: str) -> Any:
|
||||||
# your part 1 solution here
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def part_2(input_data: str) -> Any:
|
def part_2(input_data: str) -> Any:
|
||||||
# your part 2 solution here
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# alternatively you can implement a "two in one solution" like this
|
def test_part_1(example_data):
|
||||||
# part_1 and part_2 must be removed or commented out in this case
|
assert part_1(example_data) == 0
|
||||||
# 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_2(example_data):
|
||||||
|
assert part_2(example_data) == 0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ def run_day(year: int, day: int, input_data: str, path_base: pathlib.Path) -> No
|
|||||||
spec.loader.exec_module(solution_module)
|
spec.loader.exec_module(solution_module)
|
||||||
except (ModuleNotFoundError, exceptions.AocError) as e:
|
except (ModuleNotFoundError, exceptions.AocError) as e:
|
||||||
raise exceptions.AocError(
|
raise exceptions.AocError(
|
||||||
f"solution module for {year}/{day:02} not found: run 'python main.py create --year {year} {day}'"
|
f"solution module for {year}/{day:02} not found: "
|
||||||
|
f"run 'python main.py create --year {year} {day}'"
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
p_1 = getattr(solution_module, "part_1", None)
|
p_1 = getattr(solution_module, "part_1", None)
|
||||||
|
|||||||
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/03.txt
Normal file
4
data/example/2025/03.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
987654321111111
|
||||||
|
811111111111119
|
||||||
|
234234234234278
|
||||||
|
818181911112111
|
||||||
10
data/example/2025/04.txt
Normal file
10
data/example/2025/04.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
..@@.@@@@.
|
||||||
|
@@@.@.@.@@
|
||||||
|
@@@@@.@.@@
|
||||||
|
@.@@@@..@.
|
||||||
|
@@.@@@@.@@
|
||||||
|
.@@@@@@@.@
|
||||||
|
.@.@.@.@@@
|
||||||
|
@.@@@.@@@@
|
||||||
|
.@@@@@@@@.
|
||||||
|
@.@.@@@.@.
|
||||||
11
data/example/2025/05.txt
Normal file
11
data/example/2025/05.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
3-5
|
||||||
|
10-14
|
||||||
|
16-20
|
||||||
|
12-18
|
||||||
|
|
||||||
|
1
|
||||||
|
5
|
||||||
|
8
|
||||||
|
11
|
||||||
|
17
|
||||||
|
32
|
||||||
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
@@ -10,8 +10,6 @@ select = [
|
|||||||
'UP', # pyupgrade
|
'UP', # pyupgrade
|
||||||
'B', # flake8-bugbear
|
'B', # flake8-bugbear
|
||||||
'C', # flake8-comprehensions
|
'C', # flake8-comprehensions
|
||||||
'DTZ', # flake8-datetimez
|
|
||||||
'DJ', # flake8-django
|
|
||||||
'RUF', # ruff
|
'RUF', # ruff
|
||||||
'N', # pep8-naming
|
'N', # pep8-naming
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,2 +1,7 @@
|
|||||||
|
# cli
|
||||||
python-dotenv==1.2.1
|
python-dotenv==1.2.1
|
||||||
requests==2.32.5
|
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
|
||||||
64
src/solution/year_2025/day_03.py
Normal file
64
src/solution/year_2025/day_03.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def find_max_subnumber(numbers: list[int], length: int) -> int:
|
||||||
|
numbers = list(reversed(numbers))
|
||||||
|
max_search_index = len(numbers)
|
||||||
|
n = 0
|
||||||
|
|
||||||
|
if length > len(numbers):
|
||||||
|
raise ValueError("length greater than numbers length")
|
||||||
|
|
||||||
|
for start_idx in range(length - 1, -1, -1):
|
||||||
|
# local number and loacal max search index
|
||||||
|
ln, lmsi = 0, 0
|
||||||
|
# search from start_idx up to max_search_index
|
||||||
|
for idx in range(start_idx, max_search_index):
|
||||||
|
m = numbers[idx]
|
||||||
|
if idx >= max_search_index:
|
||||||
|
max_search_index = lmsi
|
||||||
|
break
|
||||||
|
# find leftmost highst number and update found digit and local max index
|
||||||
|
if m >= ln:
|
||||||
|
ln = m
|
||||||
|
lmsi = idx
|
||||||
|
|
||||||
|
max_search_index = lmsi
|
||||||
|
n += ln
|
||||||
|
n *= 10
|
||||||
|
|
||||||
|
return n // 10
|
||||||
|
|
||||||
|
|
||||||
|
def part_1(input_data: str) -> Any:
|
||||||
|
s = 0
|
||||||
|
for line in input_data.splitlines():
|
||||||
|
parsed = list(map(int, line))
|
||||||
|
s += find_max_subnumber(parsed, 2)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def part_2(input_data: str) -> Any:
|
||||||
|
s = 0
|
||||||
|
for line in input_data.splitlines():
|
||||||
|
parsed = list(map(int, line))
|
||||||
|
s += find_max_subnumber(parsed, 12)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_max_subnumber():
|
||||||
|
assert find_max_subnumber([1, 2, 3, 4, 9], 2) == 49
|
||||||
|
assert find_max_subnumber([1, 2, 3, 4, 5], 2) == 45
|
||||||
|
assert find_max_subnumber([5, 4, 3, 2, 1], 2) == 54
|
||||||
|
assert find_max_subnumber([1, 3, 5, 2, 4], 2) == 54
|
||||||
|
assert find_max_subnumber([8, 1, 8, 2, 7, 3], 3) == 887
|
||||||
|
assert find_max_subnumber([1, 9, 2, 8, 3, 7], 3) == 987
|
||||||
|
assert find_max_subnumber([1, 2, 3, 4, 5], 3) == 345
|
||||||
|
|
||||||
|
|
||||||
|
def test_part_1(example_data):
|
||||||
|
assert part_1(example_data) == 357
|
||||||
|
|
||||||
|
|
||||||
|
def test_part_2(example_data):
|
||||||
|
assert part_2(example_data) == 3121910778619
|
||||||
58
src/solution/year_2025/day_04.py
Normal file
58
src/solution/year_2025/day_04.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
dirs = [(0, 1), (0, -1), (1, 0), (-1, 0), (1, 1), (-1, -1), (-1, 1), (1, -1)]
|
||||||
|
|
||||||
|
|
||||||
|
def _count_and_remove_rolls(diagram: list[list[str]], replace: bool = False) -> int:
|
||||||
|
c = 0
|
||||||
|
for i, line in enumerate(diagram):
|
||||||
|
for j, cell in enumerate(line):
|
||||||
|
if cell != "@":
|
||||||
|
continue
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
for di, dj in dirs:
|
||||||
|
ni, nj = (i + di, j + dj)
|
||||||
|
|
||||||
|
if (
|
||||||
|
(0 <= ni < len(diagram))
|
||||||
|
and (0 <= nj < len(line))
|
||||||
|
and diagram[ni][nj] == "@"
|
||||||
|
):
|
||||||
|
n += 1
|
||||||
|
|
||||||
|
if n >= 4:
|
||||||
|
continue
|
||||||
|
|
||||||
|
c += 1
|
||||||
|
if replace:
|
||||||
|
diagram[i][j] = "."
|
||||||
|
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def part_1(input_data: str) -> Any:
|
||||||
|
diagram = [list(x) for x in input_data.splitlines()]
|
||||||
|
return _count_and_remove_rolls(diagram)
|
||||||
|
|
||||||
|
|
||||||
|
def part_2(input_data: str) -> Any:
|
||||||
|
diagram = [list(x) for x in input_data.splitlines()]
|
||||||
|
|
||||||
|
c = 0
|
||||||
|
while True:
|
||||||
|
n = _count_and_remove_rolls(diagram, True)
|
||||||
|
if n == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
c += n
|
||||||
|
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def test_part_1(example_data):
|
||||||
|
assert part_1(example_data) == 13
|
||||||
|
|
||||||
|
|
||||||
|
def test_part_2(example_data):
|
||||||
|
assert part_2(example_data) == 43
|
||||||
63
src/solution/year_2025/day_05.py
Normal file
63
src/solution/year_2025/day_05.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from typing import Any, TypeAlias
|
||||||
|
|
||||||
|
Range: TypeAlias = tuple[int, int]
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_input(input_data) -> tuple[list[Range], list[int]]:
|
||||||
|
_ranges, _ids = input_data.split("\n\n")
|
||||||
|
|
||||||
|
ranges = []
|
||||||
|
for r in _ranges.splitlines():
|
||||||
|
a, b = r.split("-")
|
||||||
|
ranges.append((int(a), int(b)))
|
||||||
|
|
||||||
|
ids = list(map(int, _ids.splitlines()))
|
||||||
|
|
||||||
|
return ranges, ids
|
||||||
|
|
||||||
|
|
||||||
|
def part_1(input_data: str) -> Any:
|
||||||
|
ranges, ids = _parse_input(input_data)
|
||||||
|
|
||||||
|
c = 0
|
||||||
|
for i in ids:
|
||||||
|
for r1, r2 in ranges:
|
||||||
|
if r1 <= i <= r2:
|
||||||
|
c += 1
|
||||||
|
break
|
||||||
|
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def part_2(input_data: str) -> Any:
|
||||||
|
ranges, _ = _parse_input(input_data)
|
||||||
|
|
||||||
|
points: list[tuple[int, int]] = []
|
||||||
|
for i, (r1, r2) in enumerate(ranges):
|
||||||
|
points.extend(((r1, i), (r2, i)))
|
||||||
|
|
||||||
|
points.sort()
|
||||||
|
|
||||||
|
c = 0
|
||||||
|
opened = set()
|
||||||
|
pp = points[0][0] - 1
|
||||||
|
for p, pid in points:
|
||||||
|
o = pid not in opened
|
||||||
|
opened.add(pid) if o else opened.remove(pid)
|
||||||
|
|
||||||
|
if not o or len(opened) > 1:
|
||||||
|
c += p - pp
|
||||||
|
elif p != pp:
|
||||||
|
c += 1
|
||||||
|
|
||||||
|
pp = p
|
||||||
|
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def test_part_1(example_data):
|
||||||
|
assert part_1(example_data) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_part_2(example_data):
|
||||||
|
assert part_2(example_data) == 14
|
||||||
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