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 = (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:

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

  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.