Skip to content

Commit 3fbf301

Browse files
committed
feat: aoc 2025 day 8 solution + tests
1 parent cfc5076 commit 3fbf301

File tree

4 files changed

+284
-0
lines changed

4 files changed

+284
-0
lines changed

_2025/solutions/day08.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
"""Day 8: Playground
2+
3+
This module provides the solution for Advent of Code 2025 - Day 8.
4+
5+
It simulates connecting electrical junction boxes in 3D space by distance
6+
to form circuits, using union-find to track component sizes as connections
7+
are added in order of increasing Euclidean distance.
8+
9+
The module contains a DSU (Disjoint Set Union) dataclass for efficient
10+
circuit merging and a Solution class that inherits from SolutionBase.
11+
"""
12+
13+
from dataclasses import dataclass
14+
import math
15+
from typing import Self
16+
17+
from aoc.models.base import SolutionBase
18+
19+
20+
@dataclass
21+
class DSU:
22+
"""Disjoint set union structure for tracking junction box circuits.
23+
24+
Maintains parent and component size arrays to efficiently perform
25+
union-find operations with path compression and union by size for
26+
optimal circuit merging.
27+
"""
28+
29+
parent: list[int]
30+
size: list[int]
31+
32+
@classmethod
33+
def with_n(cls, n: int) -> Self:
34+
"""Create DSU with n singleton junction box circuits.
35+
36+
Args:
37+
n: Number of junction boxes
38+
39+
Returns
40+
-------
41+
DSU: New instance with each box in its own circuit
42+
"""
43+
return cls(parent=list(range(n)), size=[1] * n)
44+
45+
def find(self, x: int) -> int:
46+
"""Find root representative of junction box x's circuit with path compression."""
47+
while self.parent[x] != x:
48+
self.parent[x] = self.parent[self.parent[x]]
49+
x = self.parent[x]
50+
51+
return x
52+
53+
def union(self, a: int, b: int) -> bool:
54+
"""Merge circuits containing junction boxes a and b.
55+
56+
Args:
57+
a: First junction box index
58+
b: Second junction box index
59+
60+
Returns
61+
-------
62+
bool: True if circuits were merged, False if already connected
63+
"""
64+
ra, rb = self.find(a), self.find(b)
65+
if ra == rb:
66+
return False
67+
68+
if self.size[ra] < self.size[rb]:
69+
ra, rb = rb, ra
70+
71+
self.parent[rb] = ra
72+
self.size[ra] += self.size[rb]
73+
return True
74+
75+
76+
class Solution(SolutionBase):
77+
"""Connect junction boxes by shortest distance to form electrical circuits.
78+
79+
This solution connects junction boxes in 3D space using strings of lights,
80+
always connecting the closest unconnected pair. Uses Kruskal's algorithm
81+
approach with Euclidean distance sorting and DSU for cycle detection.
82+
83+
Part 1: After 1000 shortest connections (10 for examples), multiply sizes
84+
of the three largest circuits. Part 2: Connect until single circuit, return
85+
product of X-coordinates of final merge pair.
86+
"""
87+
88+
def build_edges(self, boxes: list[list[int]]) -> list[tuple[float, int, int]]:
89+
"""Compute all pairwise Euclidean distances between junction boxes.
90+
91+
Args:
92+
boxes: List of 3D coordinates [x, y, z] for each junction box
93+
94+
Returns
95+
-------
96+
list[tuple[float, int, int]]: Sorted edges (distance, box_i, box_j)
97+
"""
98+
N = len(boxes) # noqa: N806
99+
edges: list[tuple[float, int, int]] = []
100+
for i in range(N):
101+
for j in range(i + 1, N):
102+
d = math.dist(boxes[i], boxes[j])
103+
edges.append((d, i, j))
104+
105+
edges.sort(key=lambda e: e[0])
106+
return edges
107+
108+
def find_largest_circuits(
109+
self,
110+
boxes: list[list[int]],
111+
pairs_to_process: int,
112+
) -> int:
113+
"""Process shortest connections and return product of 3 largest circuits.
114+
115+
Args:
116+
boxes: List of 3D junction box coordinates
117+
pairs_to_process: Number of shortest connections to make (1000 for real input)
118+
119+
Returns
120+
-------
121+
int: Product of sizes of three largest circuits after specified connections
122+
"""
123+
N = len(boxes) # noqa: N806
124+
edges = self.build_edges(boxes)
125+
dsu = DSU.with_n(N)
126+
127+
for processed_pairs, (_, u, v) in enumerate(edges, start=1):
128+
dsu.union(u, v)
129+
if processed_pairs == pairs_to_process:
130+
break
131+
132+
comp_sizes: dict[int, int] = {}
133+
for i in range(N):
134+
root = dsu.find(i)
135+
comp_sizes[root] = comp_sizes.get(root, 0) + 1
136+
137+
sizes = sorted(comp_sizes.values(), reverse=True)
138+
a, b, c = sizes[0], sizes[1], sizes[2]
139+
return a * b * c
140+
141+
def last_merge_x_product(self, boxes: list[list[int]]) -> int:
142+
"""Return X-coordinate product of final pair forming single circuit.
143+
144+
Args:
145+
boxes: List of 3D junction box coordinates
146+
147+
Returns
148+
-------
149+
int: Product of X coordinates of last two boxes connected
150+
151+
Raises
152+
------
153+
ValueError: If boxes don't form a single connected circuit
154+
"""
155+
N = len(boxes) # noqa: N806
156+
edges = self.build_edges(boxes)
157+
dsu = DSU.with_n(N)
158+
159+
components = N
160+
last_u: int | None = None
161+
last_v: int | None = None
162+
163+
for _, u, v in edges:
164+
if dsu.union(u, v):
165+
components -= 1
166+
last_u, last_v = u, v
167+
if components == 1:
168+
x1, x2 = boxes[last_u][0], boxes[last_v][0]
169+
return x1 * x2
170+
171+
err_msg = "Did not reach a single circuit"
172+
raise ValueError(err_msg)
173+
174+
def part1(self, data: list[str]) -> int:
175+
"""Multiply sizes of 3 largest circuits after 1000 shortest connections.
176+
177+
Uses small input (10 connections) for examples, full input (1000 connections).
178+
179+
Args:
180+
data: List of 'X,Y,Z' coordinate strings
181+
182+
Returns
183+
-------
184+
int: Product of sizes of three largest circuits after cutoff
185+
"""
186+
boxes = [list(map(int, line.split(","))) for line in data]
187+
pairs = 10 if len(boxes) <= 20 else 1000
188+
return self.find_largest_circuits(boxes, pairs_to_process=pairs)
189+
190+
def part2(self, data: list[str]) -> int:
191+
"""X-coordinate product of final pair connecting all junction boxes.
192+
193+
Continues connecting closest pairs until single circuit formed.
194+
195+
Args:
196+
data: List of 'X,Y,Z' coordinate strings
197+
198+
Returns
199+
-------
200+
int: Product of X coordinates from final successful connection
201+
"""
202+
boxes = [list(map(int, line.split(","))) for line in data]
203+
return self.last_merge_x_product(boxes)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
162,817,812
2+
57,618,57
3+
906,360,560
4+
592,479,940
5+
352,342,300
6+
466,668,158
7+
542,29,236
8+
431,825,988
9+
739,650,466
10+
52,470,668
11+
216,146,977
12+
819,987,18
13+
117,168,530
14+
805,96,715
15+
346,949,466
16+
970,615,88
17+
941,993,340
18+
862,61,35
19+
984,92,344
20+
425,690,689
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
162,817,812
2+
57,618,57
3+
906,360,560
4+
592,479,940
5+
352,342,300
6+
466,668,158
7+
542,29,236
8+
431,825,988
9+
739,650,466
10+
52,470,668
11+
216,146,977
12+
819,987,18
13+
117,168,530
14+
805,96,715
15+
346,949,466
16+
970,615,88
17+
941,993,340
18+
862,61,35
19+
984,92,344
20+
425,690,689

_2025/tests/test_08.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Test suite for Day 8: Playground
2+
3+
This module contains tests for the Day 8 solution, which connects junction
4+
boxes in 3D space by shortest distance to form electrical circuits. The tests
5+
verify:
6+
7+
1. Part 1: Product of sizes of three largest circuits after 1000 shortest connections
8+
2. Part 2: X-coordinate product of final pair forming single circuit
9+
"""
10+
11+
from aoc.models.tester import TestSolutionUtility
12+
13+
14+
def test_day08_part1() -> None:
15+
"""Test product of three largest circuits after fixed connections.
16+
17+
This test runs the solution for Part 1 of the puzzle against the
18+
provided test input and compares the result with the expected output.
19+
"""
20+
TestSolutionUtility.run_test(
21+
year=2025,
22+
day=8,
23+
is_raw=False,
24+
part_num=1,
25+
expected=40,
26+
)
27+
28+
29+
def test_day08_part2() -> None:
30+
"""Test X-coordinate product of final merge into single circuit.
31+
32+
This test runs the solution for Part 2 of the puzzle against the
33+
provided test input and compares the result with the expected output.
34+
"""
35+
TestSolutionUtility.run_test(
36+
year=2025,
37+
day=8,
38+
is_raw=False,
39+
part_num=2,
40+
expected=25272,
41+
)

0 commit comments

Comments
 (0)