|
3 | 3 | # Advent of Code 2025 Day 8 |
4 | 4 | # |
5 | 5 |
|
6 | | -import itertools |
7 | 6 | 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 |
13 | 7 | from math import prod |
14 | 8 |
|
15 | 9 | from aoc.common import InputData |
16 | 10 | from aoc.common import SolutionBase |
| 11 | +from aoc.geometry3d import Position3D |
17 | 12 |
|
18 | 13 | TEST = """\ |
19 | 14 | 162,817,812 |
|
38 | 33 | 425,690,689 |
39 | 34 | """ |
40 | 35 |
|
41 | | -Box = tuple[int, int, int] |
42 | | -Input = tuple[int, list[tuple[Box, Box]]] |
| 36 | +Input = tuple[list[Position3D], list[tuple[int, int, int]]] |
43 | 37 | Output1 = int |
44 | 38 | Output2 = int |
45 | 39 |
|
46 | 40 |
|
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 |
64 | 64 |
|
65 | 65 |
|
66 | 66 | class Solution(SolutionBase[Input, Output1, Output2]): |
67 | 67 | 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], |
89 | 79 | ) |
| 80 | + return boxes, pairs |
90 | 81 |
|
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:]) |
111 | 88 |
|
112 | 89 | 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 |
139 | 99 | raise AssertionError |
140 | 100 |
|
141 | | - def part_2(self, pairs: Input) -> Output2: |
142 | | - return self.solve_2(pairs, 1000) |
143 | | - |
144 | 101 | 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 |
148 | 105 |
|
149 | 106 |
|
150 | 107 | solution = Solution(2025, 8) |
|
0 commit comments