Skip to content

Commit f8f5264

Browse files
committed
AoC 2025 Day 8 - faster
1 parent 37e0529 commit f8f5264

File tree

2 files changed

+84
-118
lines changed

2 files changed

+84
-118
lines changed

src/main/python/AoC2025_08.py

Lines changed: 55 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,12 @@
33
# Advent of Code 2025 Day 8
44
#
55

6-
import itertools
76
import sys
8-
from collections import defaultdict
9-
from collections import deque
10-
from collections.abc import Callable
11-
from collections.abc import Iterator
12-
from functools import cmp_to_key
137
from math import prod
148

159
from aoc.common import InputData
1610
from aoc.common import SolutionBase
11+
from aoc.geometry3d import Position3D
1712

1813
TEST = """\
1914
162,817,812
@@ -38,113 +33,75 @@
3833
425,690,689
3934
"""
4035

41-
Box = tuple[int, int, int]
42-
Input = tuple[int, list[tuple[Box, Box]]]
36+
Input = tuple[list[Position3D], list[tuple[int, int, int]]]
4337
Output1 = int
4438
Output2 = int
4539

4640

47-
def connected_components(
48-
nodes: set[Box], adjacent: Callable[[Box], Iterator[Box]]
49-
) -> list[set[Box]]:
50-
components = list[set[Box]]()
51-
todo = set(nodes)
52-
while len(todo) != 0:
53-
node = todo.pop()
54-
component = {node}
55-
q = deque[Box]([node])
56-
while len(q) != 0:
57-
node = q.popleft()
58-
for n in adjacent(node):
59-
if n not in component:
60-
q.append(n)
61-
component.add(n)
62-
components.append(component)
63-
return components
41+
class DSU:
42+
def __init__(self, size: int) -> None:
43+
self.ids = list(range(size))
44+
self.sz = [1 for _ in range(size)]
45+
self.num_components = size
46+
47+
def find(self, p: int) -> int:
48+
if self.ids[p] != p:
49+
self.ids[p] = self.find(self.ids[p])
50+
return self.ids[p]
51+
52+
def unify(self, p: int, q: int) -> None:
53+
root_p, root_q = self.find(p), self.find(q)
54+
if root_p != root_q:
55+
if self.sz[root_p] < self.sz[root_q]:
56+
self.sz[root_q] += self.sz[root_p]
57+
self.ids[root_p] = root_q
58+
self.sz[root_p] = 0
59+
else:
60+
self.sz[root_p] += self.sz[root_q]
61+
self.ids[root_q] = root_p
62+
self.sz[root_q] = 0
63+
self.num_components -= 1
6464

6565

6666
class Solution(SolutionBase[Input, Output1, Output2]):
6767
def parse_input(self, input_data: InputData) -> Input:
68-
def cmp(p1: tuple[Box, Box], p2: tuple[Box, Box]) -> int:
69-
def distance(a: Box, b: Box) -> float:
70-
x1, y1, z1 = a
71-
x2, y2, z2 = b
72-
dx, dy, dz = x1 - x2, y1 - y2, z1 - z2
73-
return dx * dx + dy * dy + dz * dz
74-
75-
dp1 = distance(p1[0], p1[1])
76-
dp2 = distance(p2[0], p2[1])
77-
if dp1 > dp2:
78-
return 1
79-
if dp1 < dp2:
80-
return -1
81-
return 0
82-
83-
boxes = list[Box]()
84-
for line in input_data:
85-
x, y, z = map(int, line.split(","))
86-
boxes.append((x, y, z))
87-
return len(boxes), sorted(
88-
itertools.combinations(boxes, 2), key=cmp_to_key(cmp)
68+
boxes = [
69+
Position3D.of(int(x), int(y), int(z))
70+
for x, y, z in (line.split(",") for line in input_data)
71+
]
72+
pairs = sorted(
73+
(
74+
(i, j, boxes[i].squared_distance(boxes[j]))
75+
for i in range(len(boxes))
76+
for j in range(i + 1, len(boxes))
77+
),
78+
key=lambda pair: pair[2],
8979
)
80+
return boxes, pairs
9081

91-
def get_components(
92-
self, edges: dict[Box, set[Box]]
93-
) -> set[frozenset[Box]]:
94-
return {
95-
frozenset(c)
96-
for c in connected_components(
97-
set(edges.keys()), lambda n: (nxt for nxt in edges[n])
98-
)
99-
}
100-
101-
def solve_1(self, inputs: Input, num_pairs: int) -> int:
102-
_, pairs = inputs
103-
edges = defaultdict[Box, set[Box]](set)
104-
for i in range(num_pairs):
105-
a, b = pairs[i]
106-
edges[a].add(b)
107-
edges[b].add(a)
108-
components = self.get_components(edges)
109-
best = sorted(components, key=len, reverse=True)
110-
return prod(len(c) for c in best[:3])
82+
def solve_1(self, inputs: Input, num_pairs: int = 1000) -> int:
83+
boxes, pairs = inputs
84+
dsu = DSU(len(boxes))
85+
for pair in pairs[:num_pairs]:
86+
dsu.unify(pair[0], pair[1])
87+
return prod(sorted(dsu.sz)[-3:])
11188

11289
def part_1(self, pairs: Input) -> Output1:
113-
return self.solve_1(pairs, 1000)
114-
115-
def solve_2(self, inputs: Input, start: int) -> int:
116-
num_boxes, pairs = inputs
117-
edges = defaultdict[Box, set[Box]](set)
118-
edges = defaultdict[Box, set[Box]](set)
119-
for i in range(start):
120-
a, b = pairs[i]
121-
edges[a].add(b)
122-
edges[b].add(a)
123-
components = self.get_components(edges)
124-
while i < len(pairs) - 1:
125-
i += 1
126-
a, b = pairs[i]
127-
for c in components:
128-
if a in c and b in c:
129-
break
130-
else:
131-
edges[a].add(b)
132-
edges[b].add(a)
133-
components = self.get_components(edges)
134-
if (
135-
len(components) == 1
136-
and len(next(_ for _ in components)) == num_boxes
137-
):
138-
return pairs[i][0][0] * pairs[i][1][0]
90+
return self.solve_1(pairs)
91+
92+
def part_2(self, inputs: Input) -> Output2:
93+
boxes, pairs = inputs
94+
dsu = DSU(len(boxes))
95+
for pair in pairs:
96+
dsu.unify(pair[0], pair[1])
97+
if dsu.num_components == 1 and dsu.sz[dsu.find(0)] == len(boxes):
98+
return boxes[pair[0]].x * boxes[pair[1]].x
13999
raise AssertionError
140100

141-
def part_2(self, pairs: Input) -> Output2:
142-
return self.solve_2(pairs, 1000)
143-
144101
def samples(self) -> None:
145-
pairs = self.parse_input(TEST.splitlines())
146-
assert self.solve_1(pairs, 10) == 40
147-
assert self.solve_2(pairs, 10) == 25272
102+
inputs = self.parse_input(TEST.splitlines())
103+
assert self.solve_1(inputs, 10) == 40
104+
assert self.part_2(inputs) == 25272
148105

149106

150107
solution = Solution(2025, 8)

src/main/python/aoc/geometry3d.py

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
from __future__ import annotations
22

33
import itertools
4-
from typing import Iterator
4+
from typing import TYPE_CHECKING
55
from typing import NamedTuple
6+
from typing import Self
7+
8+
if TYPE_CHECKING:
9+
from collections.abc import Iterator
610

711
from aoc.range import RangeInclusive
812

@@ -15,41 +19,46 @@ class Point3D(NamedTuple):
1519

1620
class Position3D(Point3D):
1721
@classmethod
18-
def copy(cls, position: Position3D) -> Position3D:
19-
return Position3D.of(position.x, position.y, position.z)
22+
def copy(cls, position: Self) -> Self:
23+
return cls(position.x, position.y, position.z)
2024

2125
@classmethod
22-
def of(cls, x: int, y: int, z: int) -> Position3D:
23-
return Position3D(x, y, z)
26+
def of(cls, x: int, y: int, z: int) -> Self:
27+
return cls(x, y, z)
2428

2529
def translate(self, vector: Vector3D, amplitude: int = 1) -> Position3D:
26-
return Position3D.of(
30+
return Position3D(
2731
self.x + vector.x * amplitude,
2832
self.y + vector.y * amplitude,
2933
self.z + vector.z * amplitude,
3034
)
3135

32-
def manhattan_distance(self, other: Position3D) -> int:
36+
def manhattan_distance(self, other: Self) -> int:
3337
return (
3438
abs(self.x - other.x)
3539
+ abs(self.y - other.y)
3640
+ abs(self.z - other.z)
3741
)
3842

3943
def manhattan_distance_to_origin(self) -> int:
40-
return self.manhattan_distance(Position3D.of(0, 0, 0))
44+
return self.manhattan_distance(ORIGIN)
45+
46+
def squared_distance(self, other: Self) -> int:
47+
dx, dy, dz = self.x - other.x, self.y - other.y, self.z - other.z
48+
return dx * dx + dy * dy + dz * dz
49+
50+
51+
ORIGIN = Position3D(0, 0, 0)
4152

4253

4354
class Vector3D(Point3D):
4455
@classmethod
45-
def of(cls, x: int, y: int, z: int) -> Vector3D:
46-
return Vector3D(x, y, z)
56+
def of(cls, x: int, y: int, z: int) -> Self:
57+
return cls(x, y, z)
4758

4859
@classmethod
49-
def to_from(
50-
cls, to: Position3D, from_: Position3D = Position3D.of(0, 0, 0)
51-
) -> Vector3D:
52-
return Vector3D(to.x - from_.x, to.y - from_.y, to.z - from_.z)
60+
def to_from(cls, to: Position3D, from_: Position3D = ORIGIN) -> Self:
61+
return cls(to.x - from_.x, to.y - from_.y, to.z - from_.z)
5362

5463

5564
class Cuboid(NamedTuple):
@@ -61,10 +70,10 @@ class Cuboid(NamedTuple):
6170
z2: int
6271

6372
@classmethod
64-
def of(
73+
def of( # noqa:PLR0913
6574
cls, x1: int, x2: int, y1: int, y2: int, z1: int, z2: int
66-
) -> Cuboid:
67-
return Cuboid(x1, x2, y1, y2, z1, z2)
75+
) -> Self:
76+
return cls(x1, x2, y1, y2, z1, z2)
6877

6978
def positions(self) -> Iterator[Position3D]:
7079
for (x, y), z in itertools.product(
@@ -75,17 +84,17 @@ def positions(self) -> Iterator[Position3D]:
7584
):
7685
yield Position3D(x, y, z)
7786

78-
def overlap_x(self, other: Cuboid) -> bool:
87+
def overlap_x(self, other: Self) -> bool:
7988
return RangeInclusive.between(self.x1, self.x2).is_overlapped_by(
8089
RangeInclusive.between(other.x1, other.x2)
8190
)
8291

83-
def overlap_y(self, other: Cuboid) -> bool:
92+
def overlap_y(self, other: Self) -> bool:
8493
return RangeInclusive.between(self.y1, self.y2).is_overlapped_by(
8594
RangeInclusive.between(other.y1, other.y2)
8695
)
8796

88-
def overlap_z(self, other: Cuboid) -> bool:
97+
def overlap_z(self, other: Self) -> bool:
8998
return RangeInclusive.between(self.z1, self.z2).is_overlapped_by(
9099
RangeInclusive.between(other.z1, other.z2)
91100
)

0 commit comments

Comments
 (0)