Day 8/2025 - Junction Boxes

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:

  1. Both already in the same circuit: Do nothing
  2. Both in different circuits: Merge the circuits
  3. One in a circuit, one not: Add the loner to the existing circuit
  4. 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

  1. 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.

  2. 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.

  3. Start with all singletons: For part two, initializing each point in its own circuit simplified the logic. No more "neither in a circuit" case.

  4. Debug with small inputs: Working through the example by hand in a spreadsheet revealed the merge bug. The small input is your friend.

  5. 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.