Counting Paths Through a Graph

457 words
3 min.
Post preview image
Advent of Code 2025: Day 11 – DFS path counting with cycle detection and bitmask waypoints

Day 11 is a directed graph problem. 🗺️ The input describes nodes and their outgoing edges:

you: abc def
abc: ghi out
def: ghi
ghi: out

We need to count paths from "you" to "out".

Part 1

Count all distinct paths from "you" to "out", with cycle detection to handle possible loops in the graph.

The approach is DFS with memoization. The tricky part: standard memoization doesn’t work on its own if there are cycles, because a node’s result depends on whether we’re currently in a cycle. The solution is to use a visiting set – if we encounter a node we’re currently exploring, we return 0 (no valid path through a cycle):

function countPaths(graph: Map<string, string[]>): number {
    const memo = new Map<string, number>();
    const visiting = new Set<string>();

    const dfs = (node: string): number => {
        if (node === "out") return 1;
        const cached = memo.get(node);
        if (cached !== undefined) return cached;
        if (visiting.has(node)) return 0; // cycle detected

        visiting.add(node);
        const neighbors = graph.get(node) ?? [];
        let total = 0;
        for (const next of neighbors) {
            total += dfs(next);
        }
        visiting.delete(node);
        memo.set(node, total);
        return total;
    };

    return dfs("you");
}

Note that we add to the memo after the DFS completes (and remove from visiting). This means cycles don’t get memoized as 0 – only acyclic sub-paths get cached.


Part 2

Part 2 counts paths that pass through both "dac" and "fft" – two required waypoints.

We extend the DFS state with a bitmask tracking which required nodes we’ve visited. dac contributes bit 1, fft contributes bit 2. A path is valid at "out" only if the mask equals 3 (both bits set):

function countPathsVia(graph: Map<string, string[]>): number {
    const required = new Map<string, number>([
        ["dac", 1],
        ["fft", 2],
    ]);
    const memo = new Map<string, number>();
    const visiting = new Set<string>();

    const dfs = (node: string, mask: number): number => {
        const nextMask = mask | (required.get(node) ?? 0);
        if (node === "out") {
            return nextMask === 3 ? 1 : 0;
        }
        const key = `${node}|${nextMask}`;
        const cached = memo.get(key);
        if (cached !== undefined) return cached;
        if (visiting.has(key)) return 0;

        visiting.add(key);
        const neighbors = graph.get(node) ?? [];
        let total = 0;
        for (const next of neighbors) {
            total += dfs(next, nextMask);
        }
        visiting.delete(key);
        memo.set(key, total);
        return total;
    };

    return dfs("svr", 0);
}

The memo key is now node|mask – the same node can be visited with different masks (having seen 0, 1, or both waypoints so far), and each combination needs its own cached count.

Just two more and we’re there!


Bit-Flip Machines and Joltage Equations
Fitting Shapes into Regions