Spoiler alert

This is a full solution for Advent of Code 2025, day 11 puzzles with explanations.

If you want to try to solve it yourself first, head over to https://adventofcode.com/2025/day/11 and give it a go and then come back and compare notes.

You can find the full code at https://github.com/Hamatti/adventofcode-2025/blob/main/src/day_11.py

We’ve reached a toroidal reactor but the new server rack is having issues controlling it.

Read input

We return to the world of simple inputs. Our input is a mapping of start: node1 node2 ..., like

aaa: you hhh
you: bbb ccc
bbb: ddd eee
ccc: ddd eee fff
ddd: ggg
eee: out
fff: out
ggg: out
hhh: ccc fff iii
iii: out

It makes our mapper function much more straight-forward than for example yesterday.

def mapper(line: str) -> Tuple[str, List[str]]:
    root, nodes = line.split(': ')
    nodes = nodes.split(' ')
 
    return root, nodes

Part 1

In the first part, we need to calculate in how many ways we can reach node out when we start from you.

EXIT_NODE = 'out'
 
def find_path(root: str, nodes: dict[str, List[str]]) -> int:
    if EXIT_NODE in nodes[root]:
        return 1
 
    ways = 0
    for node in nodes[root]:
        ways += find_path(node, nodes)
    return ways

We use a recursive solution that starts from a root node, checks if it leads to EXIT_NODE and if it does, we count it to our total. If there’s no such node, we go through all the different nodes our current one leads to and the algorithm pops out a total number.

P1_START_NODE = 'you'
 
def part_1() -> int:
    """Calculate in how many ways you can reach node EXIT_NODE
    when starting from node P1_START_NODE."""
    data = read_input(11, mapper)
    tree = {}
    for root, nodes in data:
        tree[root] = nodes
 
    return find_path(P1_START_NODE, tree)

We then reshape the data into a dictionary for easy access and let it run.

Part 2

Recursion is my nemesis. Somehow, despite years of working with these puzzles, I still always struggle with the same things, as if I had not learned anything.

Here, the goal is to find all paths that lead from svr to out but only if they pass through nodes dac and fft. The trick here is that a brute-force solution of checking every path is not viable. I knew right away that I need to use memoisation, a technique where we store known results and then pick them from the cache the next time rather than following the full path down the line every time.

The tricky bit with caching is that Python’s functools.cache can only cache functions where all the parameters are hashable. So no lists, sets or dicts. I managed to write a functional solution for the example input (after realising the part 2 had a different example input) but it was not feasible for the main input.

I had a discussion about it with a colleague that led me to realise that I don’t have to cache the entire path but only the root and whether we’ve already visit dac and fft.

from functools import cache
 
@cache
def fft_dac_path(root: str, nodes: Tuple[str, Tuple[str, ...]], seen_fft: bool=False, seen_dac:bool=False) -> int:
    """Follow the path from root node until EXIT_NODE node.
    
    If a path contains both 'dac' and 'fft' nodes, count it,
    otherwise ignore it."""
    seen_dac = seen_dac or root == 'dac'
    seen_fft = seen_fft or root == 'fft'
 
    next_nodes = [n for r, n in nodes if r == root][0]
    
    if EXIT_NODE in next_nodes:
        if seen_fft and seen_dac:
            return 1
        else:
            return 0
    
    valid_paths = 0
    for node in next_nodes:
        valid_paths += fft_dac_path(node, nodes, seen_fft, seen_dac)
    
    return valid_paths

@cache before the function declaration is called a decorator and it’s a way to wrap functions inside other functions. In this case, @cache keeps track of every function call: its parameters and its output. If it later sees the same set of parameters being used, it will directly pick the result from the cache, thus saving immense amount of processing.

To be able to use caching, I also had to use tuples instead of dict to map the path since tuples are immutable and thus hashable. Otherwise, it’s pretty much the same solution as in the first part but it takes into account whether we’ve visited dac and fft already.

def part_2() -> int:
    """Starting from P2_START_NODE, calculate how many
    ways there are to reach EXIT_NODE while visiting both
    'fft' and 'dac'"""
    data = read_input(11, mapper)
    tree = []
    for root, nodes in data:
        tree.append((root, tuple(nodes)))
    tree = tuple(tree)
    
    valid_paths = fft_dac_path(P2_START_NODE, tree)
    return valid_paths

After yesterday’s 1-star performance, we’re back to 2 stars and before tomorrow’s last day, we’re at 21/24 stars. Santa would be proud of us!

Advent of Code jam day is a great day

For the second year now, our local Python meetup archipylago is hosting a Thursday evening “solve puzzles and have fun” Advent of Code sprint event where people come to solve problems, learn new tricks, get help and have a great time with each other before everyone heads out for holidays.

Get in touch

If you want to comment on these solutions, I share them in Mastodon where you can look for the post for the right date or you can start a discussion over email with juhamattisantala@gmail.com.

Follow via RSS

I have created a custom RSS feed for these solutions so you can follow along and not miss any of them!