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 = (point: Point): string => `${point.x},${point.y},${point.z}`;
function getDistances(input: Point[]) {
const distances: Distance[] = [];
for (let findex = 0; findex < input.length; findex++) {
const first = input[findex];
for (let sindex = findex + 1; sindex < input.length; sindex++) {
const second = input[sindex];
const dx = first.x - second.x;
const dy = first.y - second.y;
const dz = first.z - second.z;
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
distances.push({
first: toKey(first),
second: toKey(second),
distance: distance
});
}
}
distances.sort((a, b) => a.distance - b.distance);
return distances;
}
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 topJunctionCount = 1000;
const distances = getDistances(input);
const junctions = distances.slice(0, topJunctionCount);
const circuits: Set<string>[] = [];
for (const {first, second} of junctions) {
let firstCircuit = circuits.find(c => c.has(first));
let secondCircuit = circuits.find(c => c.has(second));
if (firstCircuit && secondCircuit) {
if (firstCircuit !== secondCircuit) {
// Merge circuits
const joinedCircuit = new Set([...firstCircuit, ...secondCircuit]);
circuits.splice(circuits.indexOf(secondCircuit), 1);
circuits[circuits.indexOf(firstCircuit)] = joinedCircuit;
}
} else if (firstCircuit) {
firstCircuit.add(second);
} else if (secondCircuit) {
secondCircuit.add(first);
} else {
// Create new circuit
circuits.push(new Set([first, second]));
}
}
// Sort by size, multiply top 3
circuits.sort((a, b) => b.size - a.size);
const [topOne, topTwo, topThree] = circuits;
const result = topOne.size * (topTwo ? topTwo.size : 1) * (topThree ? topThree.size : 1);
return result;
};
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);
const circuits: Set<string>[] = input.map(p => new Set([toKey(p)]));
for (const {first, second} of distances) {
let firstCircuit = circuits.find(c => c.has(first))!;
let secondCircuit = circuits.find(c => c.has(second))!;
if (firstCircuit !== secondCircuit) {
// Merge circuits
const joinedCircuit = new Set([...firstCircuit, ...secondCircuit]);
circuits.splice(circuits.indexOf(secondCircuit), 1);
circuits[circuits.indexOf(firstCircuit)] = joinedCircuit;
}
if (circuits.length == 1) {
// Found it! Return product of x-coordinates
const fx = first.split(",").map(Number)[0];
const sx = second.split(",").map(Number)[0];
return fx * sx;
}
}
throw new Error("Could not 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.