Advent of Code 2025 Day 11 Part 2 Explained - Reactor
You can find my Rust implementation on Github.
Part 1 — 😌
No difficulties here. I solved it with a simple BFS that generates all possible paths.
Part 2 — 🥵
This one was a real pain… until I found a super simple solution 😅.
Usually I’m comfortable with graph problems, but the dac and fft devices bothered me. I also got stubborn about using BFS. When I switched later to DFS, everything was much easier.
First Try - Changing the minimum amount of code
Part 1 worked perfectly with a simple BFS.
So my first attempt was just a small change: I kept track of the devices visited by each path. Then, when reaching out, I incremented the counter only if that path had visited both dac and fft.
#[derive(Debug)]
struct PathExplorer {
device: String,
visited: HashSet<String>,
}
let graph = read_rotations();
let mut paths_count = 0;
let mut deque: VecDeque<PathExplorer> = VecDeque::new();
deque.push_back(PathExplorer {
device: "svr".to_string(),
visited: HashSet::from(["svr".to_string()]),
});
while let Some(p) = deque.pop_front() {
if p.device == "out" {
if p.visited.contains("dac") && p.visited.contains("fft") {
paths_count += 1;
}
} else {
for device in graph.get(&p.device).unwrap() {
if !p.visited.contains(device.as_str()) {
let mut next_p = PathExplorer {
device: device.clone(),
visited: p.visited.clone(),
};
next_p.visited.insert(device.clone());
deque.push_back(next_p);
}
}
}
}
println!("{:?}", paths_count);
But this didn’t work: the deque just kept growing and growing forever.
Maybe there’s a loop?
At first I suspected a cycle somewhere in the graph. I added logic to prevent loops and some logging. But there were loops.
Rule Number 1: If you don’t know, visualize
So I generated a .dot file from the input and rendered the graph.
Here’s what it looks like 🤯
At the bottom in green, we can see the device out.
The 2 devices in pink are our entry points: svr and you.
The 2 devices in red are dac and fft.
The first thing to notice is that the graph we had to explore in Part 1 was tiny compared to Part 2.
This explains why the deque blows up: the number of possible paths is now huge.
Another thing that stands out in the visualization: dac and fft are a bit off to the side.
A new idea
Maybe a lot of nodes can be skipped. Maybe we can shrink the graph enough to reuse the Part-1 algorithm.
My idea was to eliminate any node that doesn’t eventually lead to out, dac, or fft.
Then, in this loop:
for device in graph.get(&p.device).unwrap() {
…I’d only push the devices that are eligible:
let eligible = upstream_out.contains(device)
&& (p.visited.contains("dac") || upstream_dac.contains(device))
&& (p.visited.contains("fft") || upstream_fft.contains(device));
Meaning:
- the
devicemust be able to reachout - if
dachasn’t been visited yet, thedevicemust be able to reachdac - if
ffthasn’t been visited yet, thedevicemust be able to reachfft
But this didn’t help much, the graph was still too big.
Added some logs to understand the deque
I decided to log every PathExplorer popped from the deque, and it quickly became obvious: we were visiting the same subpaths way too many times. That’s why the deque kept growing endlessly.
I tried to think of some way to make memoization for BFS, but my brain needed some fresh air.
Rule Number 2: if visualization fails, go for a walk
While walking in the park, I decided I would try DFS instead.
It simply makes more sense here:
- We go all the way down a path
- We immediately know whether we reached
outwith bothdacandfftvisited - If yes → +1, we found one valid path
- Then we backtrack one step, explore the other children, backtrack again, and so on…
For this challenge, DFS just feels more natural. And I decided to implement it recursively, which is usually simpler.
The only thing I didn’t know at that point was how I would do memoization. But it happened naturally.
fn dfs(
device: String,
fft: bool,
dac: bool,
graph: &HashMap<String, Vec<String>>,
memo: &mut HashMap<(String, bool, bool), usize>,
) -> usize {
if device == "out" {
return if fft && dac { 1 } else { 0 };
}
let mut paths_count = 0;
let next_fft = fft || device == "fft";
let next_dac: bool = dac || device == "dac";
for next_device in graph.get(&device).unwrap() {
if let Some(m) = memo.get(&(next_device.to_string(), next_fft, next_dac)) {
paths_count += m;
} else {
let r = dfs(next_device.to_string(), next_fft, next_dac, graph, memo);
paths_count += r;
memo.insert((next_device.clone(), next_fft, next_dac), r);
}
}
paths_count
}
For the memoization, we want to remember:
“I’ve already reached this node with this state → here’s the result of exploring the rest of the graph from here with this state.”
And the state is just the arguments of the dfs function:
device: String,
fft: bool,
dac: bool,
That’s all.