Day eight of AoC 2025 (available at https://adventofcode.com/2025/day/8) brings geometry — but don't panic, it's manageable. We have junction boxes in 3D space, and we need to connect them based on which ones are closer together.
The input is straightforward: three comma-separated coordinates per line, one junction box per line.
Parsing the Input
type Point = { x: number; y: number; z: number };
const processInput = (day: number): Point[] => {
const lines = readInputLines(day);
return lines.map(line => {
const [x, y, z] = line.split(",").map(Number);
return { x, y, z };
});
};
All coordinates are positive, so we're working in a single octant. No sign shenanigans to worry about.
Calculating Distances
We need Euclidean distance in 3D — the square root of the sum of squared differences:
type Distance = {
first: string;
second: string;
distance: number;
};
const toKey = (p: Point): string => `${p.x} ${p.y} ${p.z}`;
const getDistances = (points: Point[]): Distance[] => {
const distances: Distance[] = [];
for (let i = 0; i < points.length; i++) {
for (let j = i + 1; j < points.length; j++) {
const p1 = points[i];
const p2 = points[j];
const distance = Math.sqrt(
(p1.x - p2.x) ** 2 +
(p1.y - p2.y) ** 2 +
(p1.z - p2.z) ** 2
);
distances.push({
first: toKey(p1),
second: toKey(p2),
distance
});
}
}
return distances.sort((a, b) => a.distance - b.distance);
};
For 1,000 junction boxes, that's about 500,000 distance calculations. Sorted by distance, we can process connections from shortest to longest.
Part One: Finding Circuits
We connect the 10 (or 1,000 for the real input) closest pairs and count how many separate circuits form. A circuit is any group of connected junction boxes.
This is essentially the union-find problem. When we connect two junction boxes:
- Both already in the same circuit: Do nothing
- Both in different circuits: Merge the circuits
- One in a circuit, one not: Add the loner to the existing circuit
- Neither in a circuit: Create a new circuit with both
const partOne = (input: Point[], debug: boolean) => {
const distances = getDistances(input);
const junctions = distances.slice(0, 1000); // Top N closest
const circuits: Set<string>[] = [];
for (const { first, second } of junctions) {
const firstCircuit = circuits.find(c => c.has(first));
const secondCircuit = circuits.find(c => c.has(second));
if (firstCircuit && secondCircuit) {
if (firstCircuit !== secondCircuit) {
// Merge circuits
const merged = new Set([...firstCircuit, ...secondCircuit]);
const secondIndex = circuits.indexOf(secondCircuit);
circuits.splice(secondIndex, 1);
const firstIndex = circuits.indexOf(firstCircuit);
circuits[firstIndex] = merged;
}
// Same circuit: do nothing
} else if (firstCircuit) {
firstCircuit.add(second);
} else if (secondCircuit) {
secondCircuit.add(first);
} else {
circuits.push(new Set([first, second]));
}
}
// Sort by size, multiply top 3
circuits.sort((a, b) => b.size - a.size);
const [top1, top2, top3] = circuits;
return (top1?.size ?? 1) * (top2?.size ?? 1) * (top3?.size ?? 1);
};
The Merge Bug
My first implementation had a subtle bug in the merge case:
// Wrong!
firstCircuit = new Set([...firstCircuit, ...secondCircuit]);
circuits.splice(circuits.indexOf(secondCircuit), 1);
// Now indexOf(firstCircuit) returns -1 because we replaced the reference!
By reassigning firstCircuit to a new Set, the original reference in the circuits array was unchanged. When I tried to find it again to update it, it wasn't there anymore.
The fix: create the merged set separately, then update the array:
const merged = new Set([...firstCircuit, ...secondCircuit]);
// Remove second first (before indices shift)
circuits.splice(circuits.indexOf(secondCircuit), 1);
// Now update first
circuits[circuits.indexOf(firstCircuit)] = merged;
JavaScript reference semantics strike again.
Part Two: One Big Circuit
Part two asks: keep connecting until all junction boxes form a single circuit. Which connection makes that happen?
Now we need to process all distances (sorted), not just the top N. We start with each junction box in its own circuit:
const partTwo = (input: Point[], debug: boolean) => {
const distances = getDistances(input);
// Start with each point in its own circuit
const circuits: Set<string>[] = input.map(p => new Set([toKey(p)]));
for (const { first, second } of distances) {
const firstCircuit = circuits.find(c => c.has(first))!;
const secondCircuit = circuits.find(c => c.has(second))!;
if (firstCircuit !== secondCircuit) {
// Merge circuits
const merged = new Set([...firstCircuit, ...secondCircuit]);
circuits.splice(circuits.indexOf(secondCircuit), 1);
circuits[circuits.indexOf(firstCircuit)] = merged;
if (circuits.length === 1) {
// Found it! Return product of x-coordinates
const [fx] = first.split(" ").map(Number);
const [sx] = second.split(" ").map(Number);
return fx * sx;
}
}
}
throw new Error("Couldn't connect all circuits");
};
Starting with 1,000 circuits, each merge reduces the count by one. We watch the count drop: 999, 998, 997... until we hit 1.
Performance
Part one runs quickly — we only process 1,000 connections. Part two takes almost a full second because we might need to process many more connections before achieving a single circuit.
A proper union-find data structure with path compression would be faster, but for this input size, the naive approach works fine.
Takeaways
Union-find in disguise: This is the classic disjoint set problem. Recognizing it helps you know what operations you need: find which set an element belongs to, merge two sets.
Reference vs value: The merge bug was a classic JavaScript gotcha. When you reassign a variable, you're not modifying the original object — you're pointing to a new one.
Start with all singletons: For part two, initializing each point in its own circuit simplified the logic. No more "neither in a circuit" case.
Debug with small inputs: Working through the example by hand in a spreadsheet revealed the merge bug. The small input is your friend.
String keys for object identity: JavaScript can't use objects as Set elements meaningfully (no structural equality). Converting points to strings gives us usable keys.
The full solution is available in the repository.