I think the hack was just skipping the entire second set of paths, but yeah, splitting was an nice optimisation. Might not scale as well if that path had to hit multiple intermediate nodes though.
comments
Yeah, ill have to bust out the pen and paper as well. Good learning excercise though :) Edit: If you find any good resources, please share :)
nushell
I'm still travelling, so another phone attempt. Jet lag says sleep, so just part 1 for now:
def part1 [filename: string] {
mut input = open $filename | lines |
each { parse '{index}: {children}' | update children { split row " " } | first } |
insert paths { null }
print $"Data loaded, ($input | length) devices"
$input = explore-path $input you
$input | where index == you | get 0.paths
}
def explore-path [devices, start: string] {
print $"Exploring ($start)"
let dev = $devices | where index == $start | first
if ($dev | get paths) != null {
print "Already explored"
return $devices
}
# Shadow with mutable version
mut devices = $devices
mut paths = 0
let is_out = $dev | get children | where ($it == out) | is-not-empty
if $is_out {
print $"Found an out device: ($start)"
$paths = 1
} else {
for child in ($dev | get children ) {
$devices = explore-path $devices $child
$paths += $devices | where index == $child | get 0.paths
}
}
# Shadow with immutable... wtf
let paths = $paths
print $"Setting paths for ($start) to ($paths)"
$devices = $devices | update paths { |row| if $row.index == $start {
$paths } else {} }
$devices
}
Good luck! If anything, yesterday's problem is motivating (if not challenging) because I'm trying to use it to learn the simplex algorithm (off mediocre online tutorials—doubly so considering I think the problem needs dual simplex [pun intended]). I've spent far more time with pencil and paper trying to understand the algorithm than actually try writing code for it.
If you can explain why it works, not calculating half the paths isn't a hack—it's an optimization. ;)
Rust
Somehow got the right answer for part 1 iteratively, but it wasn't actually correct at all, so switched to the standard recursion + memoisation. Gotta love just chucking an iterator into the value type of a HashMap and going BRRRR (1.2 ms).
solution
pub fn day11(input: &str) -> (u64, u64) {
let array = input.trim().lines();
let mut conns = HashMap::new();
for line in array {
let mut iter = line.split_whitespace();
conns.insert(&iter.next().unwrap()[0..3], iter);
}
fn find_path<'a>(
start: &'a str,
end: &'a str,
conns: &HashMap<&'a str, SplitWhitespace<'a>>,
devices: &mut HashMap<&'a str, u64>,
) -> u64 {
let mut sum = 0;
let Some(list) = conns.get(start) else {
return 0;
};
for i in list.clone() {
if i == end {
sum += 1;
} else if let Some(precomp) = devices.get(i) {
sum += precomp;
} else {
sum += find_path(i, end, conns, devices);
}
}
devices.insert(start, sum);
sum
}
let part1 = find_path("you", "out", &conns, &mut HashMap::new());
// If dac -> fft and fft -> dac are both non-zero, then there are loops. That makes an
// infinite number of paths so the question posed would be meaningless.
let dac_to_fft = find_path("dac", "fft", &conns, &mut HashMap::new());
let part2 = if dac_to_fft == 0 {
find_path("svr", "fft", &conns, &mut HashMap::new())
* find_path("fft", "dac", &conns, &mut HashMap::new())
* find_path("dac", "out", &conns, &mut HashMap::new())
} else {
find_path("svr", "dac", &conns, &mut HashMap::new())
* dac_to_fft
* find_path("fft", "out", &conns, &mut HashMap::new())
};
(part1, part2)
}
I solved with geo as well. Brute force to all possible rectangles from the polygon points. Tried to run it but after 30 seckilled it and just imported rayon. It's under 3 sec. There is no shame using libraries, it's part of the puzzle to know whics one is useful 😄
Kotlin
Part 1 had me assuming, like a lot of other folks, that Part 2 would be a simple weighted graph. So I coded Part 1 as a non-weighted graph and it was great.
Part 2 looked simple enough, but different. Part 1 was breadth first, I assumed depth first would work for Part 2. When it worked great on the test input, but took 12 seconds, I knew I was in trouble. So I added caching. And it was super quick on the test input.
Real input was a whole 'nother story. I watched my system resources balloon. At last count it was using 8GB of RAM before I killed it. And that was before solving even the first line.
So I went online to find what I'm missing to see people saying it's a linear algebra problem, and that it's best to use some kind of library for it.
I will admit that I leaned pretty heavily on asking Gemini questions to figure out how to use the Google OR-Tools library.
So here's my Part 2 code:
import com.google.ortools.Loader
import com.google.ortools.linearsolver.MPSolver
import com.google.ortools.linearsolver.MPVariable
import utils.*
fun main() {
val input = getInput(10)
val machines = parseInput1(input)
Loader.loadNativeLibraries()
var total = 0
for (machine in machines) {
val buttons = machine.buttons
val joltages = machine.joltages
val solver = MPSolver.createSolver("SCIP") ?: throw Exception("Could not create solver")
val x = arrayOfNulls<MPVariable>(machine.buttons.size)
for (i in buttons.indices) {
x[i] = solver.makeIntVar(0.0, Double.POSITIVE_INFINITY, "x$i")
}
val target = joltages.map { it.toDouble() }
val aMatrix = joltages.indices.map { joltageToArray(it, buttons) }.toTypedArray()
for (j in joltages.indices) {
val ct = solver.makeConstraint(target[j], target[j], "joltage_constraint_$j")
for (i in buttons.indices) {
ct.setCoefficient(x[i], aMatrix[j][i])
}
}
val objective = solver.objective()
for (i in buttons.indices) {
objective.setCoefficient(x[i], 1.0)
}
objective.setMinimization()
val resultStatus = solver.solve()
if (resultStatus == MPSolver.ResultStatus.OPTIMAL) {
val result = objective.value().toInt()
total += result
} else {
println("Problem could not be solved.")
}
}
println(total)
}
data class Machine(val configuration: List<Boolean>, val buttons: List<List<Int>>, val joltages: List<Int>)
fun parseInput1(input: String): List<Machine> {
return input.lines()
.filter { it.isNotBlank() }
.map {
val split = it.split(" ")
val configuration = split.first().toCharArray()
.slice(1..<split.first().length - 1)
.map { char ->
when (char) {
'#' -> true
else -> false
}
}
val buttons = split.slice(1..<split.size - 1)
.map { str ->
str.slice(1..<str.length - 1)
.split(",")
.map { number -> number.toInt() }
}
val joltages = split.last()
.slice(1..<split.last().length - 1)
.split(",")
.map { number -> number.toInt() }
Machine(configuration, buttons, joltages)
}
}
fun joltageToArray(joltageIndex: Int, buttons: List<List<Int>>): Array<Double> {
val array = DoubleArray(buttons.size) { 0.0 }.toTypedArray()
for (i in buttons.indices) {
if (joltageIndex in buttons[i]) {
array[i] = 1.0
}
}
return array
}
Joined as well, thanks for creating it! Unfortunately I only have time 5-6 hours before the next day's puzzle, so I am here only to collect 🌞s.
I'll check if someone created a guide for the clever solution, cos I'm interested. But I don't have nearly enough knowledge about geometry to came up with one myshelf. (nor energy after the 10 hour workdays in the last two weeks..)
I did linear algebra with sympy over Q to reduce the search space to nullspace x [-N,N] grid (which is generally <=2 dimensional in these inputs and N<50 seems sufficient in most cases) then made easy work of it with vectorization. Similarly for part 2 did linear algebra over F2 but the search grid is also much smaller [0,1] x nullspace
yea becomes trivial if one uses a polygon library (I did too) but looking at the actual polygon, one can cook up some clever heuristics to do it quite quickly with some basic checks I think.
The graph in my input (and I assume everyone else's too) is DAG so one can use transfer matrices for the first part (after severing connections going out of "out") and the second one topological sorting and just count paths between each node separately and multiply them. First part could be done much easily using the machinery of part2 but I wanted to use my favourite object transfer matrices for the solution. Second part runs in about 0.09 seconds.
from pathlib import Path
import networkx as nx
import numpy as np
cwd = Path(__file__).parent.resolve()
def parse_input(file_path):
with file_path.open("r") as fp:
data = list(map(str.strip, fp.readlines()))
nodes = [y for line in data for y in line.replace(':','').split(' ')]
M = np.zeros((len(nodes), len(nodes)))
for line in data:
from_node = line.split(':')[0]
to_nodes = line.split(': ')[-1].split(' ')
I0 = nodes.index(from_node)
I1 = [nodes.index(n) for n in to_nodes]
M[I0, I1] = 1
return M, nodes
def count_paths_between(ind0, ind1, M):
'''
counts paths by severing any outgoing connection from node ind1
and using transfer matrices. Stopping condition is having the
same positive number of paths for 10 cycles.
'''
vec = np.zeros((M.shape[0]))
vec[ind0] = 1
nhistory = []
A = M.T.copy()
A[:, ind1] = 0
A[ind1, ind1] = 1
counter = 0
while True:
vec = A@vec
nhistory.append(vec[ind1])
counter +=1
if len(nhistory)>10 and (len(set(nhistory[-10:]))==1 and nhistory[-1]!=0):
return nhistory[-1]
def count_paths_dag(G, source, target):
npaths = {node: 0 for node in G.nodes()}
npaths[source] = 1
for node in nx.topological_sort(G):
for nbr in G.successors(node):
npaths[nbr] += npaths[node]
return npaths[target]
def solve_problem1(file_name, path_nodes=None):
M, nodes = parse_input(Path(cwd, file_name))
if path_nodes is None:
npaths = count_paths_between(nodes.index("you"), nodes.index("out"), M)
else:
G = nx.from_numpy_array(M, create_using=nx.DiGraph(),
nodelist=nodes)
# assumed G is a DAG, below will raise error if not
sorted_nodes = list(nx.topological_sort(G))
sorted_path_nodes = sorted(path_nodes, key=sorted_nodes.index)
#make sure path nodes are not topoligically equivalent
for node1, node2 in zip(sorted_path_nodes[:-1], sorted_path_nodes[1:]):
assert nx.has_path(G, node1, node2)
npaths = np.prod([count_paths_dag(G, node1, node2) for node1, node2 in
zip(sorted_path_nodes[:-1], sorted_path_nodes[1:])])
return npaths
if __name__ == "__main__":
assert solve_problem1("test_input1") == 5
assert solve_problem1("input") == 431
assert solve_problem1("test_input2", ["svr","dac","fft","out"]) == 2
assert solve_problem1("input", ["svr","dac","fft","out"]) == 358458157650450
Kotlin
This was substantially easier than yesterday's problem. Especially considering I still haven't done Part 2 from yesterday.
Anyway, here is my Part 2 solution for today. Given how quickly Part 1 ran without a cache, I didn't think Part 2 would be much different, until my computer fans spun up and stayed there for over a minute. So I added a cache and re-ran it and it ran in 31ms.
const val end = "out"
var map: Map<String, List<String>>? = null
val problemVertices = "dac" to "fft"
val cache: MutableMap<Pair<String, Pair<Boolean, Boolean>>, Long> = mutableMapOf()
fun main() {
val input = getInput(11)
map = parseInput1(input)
val total = countPaths2("svr", false to false).first
println(total)
}
fun countPaths2(vertex: String, problemVerticesEncountered: Pair<Boolean, Boolean>): Pair<Long, Pair<Boolean, Boolean>> {
val otherVertices = map!![vertex]!!
var total = 0L
val nextProblemVerticesEncountered =
(problemVerticesEncountered.first || vertex == problemVertices.first) to (problemVerticesEncountered.second || vertex == problemVertices.second)
for (otherVertex in otherVertices) {
val key = otherVertex to nextProblemVerticesEncountered
if (cache.contains(key)) {
total += cache[key]!!
} else if (otherVertex == end) {
if (nextProblemVerticesEncountered.first && nextProblemVerticesEncountered.second) {
total++
}
} else {
total += countPaths2(otherVertex, nextProblemVerticesEncountered).first
}
}
cache[vertex to nextProblemVerticesEncountered] = total
return total to nextProblemVerticesEncountered
}
fun parseInput1(input: String): Map<String, List<String>> = input.lines()
.filter { it.isNotBlank() }
.associate {
val split = it.split(": ")
split[0] to split[1].split(" ").toList()
}
It was easy staight forward brtue force:
Spoiler
For each possible_rectange Find Max:
If polygon.contains(rectangle) Then rectangle.area() Else 0
For speed the loop can be run paralelley.
I had no time to implent the contains myself, but pretty much all languages have a library already.
Yes, it felt a little more like I was solving the puzzle-setter rather than the puzzle today.
I also broke up the pathing, mine has zero paths from DAC to FFT. If I join DAC directly FFT, i get a stackoverflow, so maybe its intentional.
Rust
Finally got it, but had to use Z3. Getting Z3 to work was a real pain, they really need to doco the rust crate better.
I'll try redo it at some point without Z3, maybe after I forgot what a pain this was.
spoiler
struct Puzzle2 {
target: Vec<u16>,
buttons: Vec<Vec<u16>>,
}
fn solve_puzzle_z3(puzzle2: &Puzzle2) -> usize {
let optimiser = Optimize::new();
let num_presses: Vec<Int> = (0..puzzle2.buttons.len())
.map(|i| Int::new_const(format!("n_{i}")))
.collect();
num_presses.iter().for_each(|p| optimiser.assert(&p.ge(0)));
(0..puzzle2.target.len()).for_each(|i| {
let r = puzzle2.target[i];
let buttons = puzzle2
.buttons
.iter()
.enumerate()
.filter_map(|(j, b)| {
if b.contains(&(i as u16)) {
Some(&num_presses[j])
} else {
None
}
})
.collect::<Vec<&Int>>();
let sum = buttons.into_iter().sum::<Int>();
optimiser.assert(&sum.eq(r));
});
let all_presses = num_presses.iter().sum::<Int>();
optimiser.minimize(&all_presses);
if optimiser.check(&[]) != SatResult::Sat {panic!("Unsolvable")}
let model = optimiser
.get_model()
.expect("Optimizer should have a model if sat");
let t = model.eval(&all_presses, true).unwrap().as_i64().unwrap();
t as usize
}
#[test]
fn test_y2025_day10_part2() {
let input = include_str!("../../input/2025/day_10.txt");
let puzzles = input
.lines()
.map(|line| {
let parts = line.split(" ").collect::<Vec<_>>();
let display = parts.last().unwrap();
let display = display[1..display.len() - 1]
.split(',')
.map(|c| c.parse::<u16>().unwrap())
.collect::<Vec<u16>>();
let buttons = parts[1..parts.len() - 1]
.iter()
.map(|s| {
s[1..s.len() - 1]
.split(",")
.map(|s| s.parse::<u16>().unwrap())
.collect::<Vec<u16>>()
})
.collect::<Vec<Vec<u16>>>();
Puzzle2 {
target: display,
buttons,
}
})
.collect::<Vec<Puzzle2>>();
let mut min_presses = 0;
for puzzle in &puzzles {
min_presses += solve_puzzle_z3(puzzle)
}
println!("PRESSED: {}", min_presses);
}
Go
Yeeeaaay graphs! This one has been a pleasure to program and here comes my solution, with pruning of irrelevant branches (cursed nodes) and memoïzation for better performance.
day11.go
package main
import (
"aoc/utils"
"fmt"
"slices"
"strings"
)
type graph map[string][]string
func graphFromInput(input chan string) graph {
g := graph{}
for line := range input {
firstSplit := strings.Split(line, ":")
currentNode := firstSplit[0]
followers := strings.Fields(firstSplit[1])
g[currentNode] = followers
}
return g
}
var memo = make(map[string]map[string]int)
func (g *graph) dfsUntil(currentNode, targetNode string, cursedNodes map[string]any) int {
subMemo, ok := memo[currentNode]
if !ok {
memo[currentNode] = make(map[string]int)
} else {
sum, ok := subMemo[targetNode]
if ok {
return sum
}
}
followers, _ := (*g)[currentNode]
sum := 0
for _, follower := range followers {
if follower == targetNode {
memo[currentNode][targetNode] = 1
return 1
} else if _, ok := cursedNodes[follower]; ok {
continue
}
sum += g.dfsUntil(follower, targetNode, cursedNodes)
}
memo[currentNode][targetNode] = sum
return sum
}
func stepOne(input chan string) (int, error) {
g := graphFromInput(input)
return g.dfsUntil("you", "out", make(map[string]any)), nil
}
func (g *graph) dfsFromSvrToOut(
currentNode string, hasDac, hasFft bool, cursedNodes map[string]any) int {
if currentNode == "dac" && hasFft {
return g.dfsUntil("dac", "out", cursedNodes)
}
hasDac = hasDac || currentNode == "dac"
hasFft = hasFft || currentNode == "fft"
followers := (*g)[currentNode]
sum := 0
for _, follower := range followers {
switch follower {
case "out":
if hasDac && hasFft {
return 1
} else {
return 0
}
default:
sum += g.dfsFromSvrToOut(follower, hasDac, hasFft, cursedNodes)
}
}
return sum
}
func (g *graph) getCursedNodes() map[string]any {
cursedNodes := make(map[string]any)
for node := range *g {
if node == "dac" || node == "fft" || node == "svr" {
continue
}
fftToNode := g.dfsUntil("fft", node, cursedNodes)
dacToNode := g.dfsUntil("dac", node, cursedNodes)
nodeToFft := g.dfsUntil(node, "fft", cursedNodes)
nodeToDac := g.dfsUntil(node, "dac", cursedNodes)
if dacToNode > 0 {
continue
}
if fftToNode > 0 && nodeToDac > 0 {
continue
}
if nodeToFft == 0 {
cursedNodes[node] = nil
}
}
return cursedNodes
}
func stepTwo(input chan string) (int, error) {
g := graphFromInput(input)
cursedNodes := g.getCursedNodes()
for cursedKey := range cursedNodes {
delete(g, cursedKey)
for gkey := range g {
g[gkey] = slices.DeleteFunc(g[gkey], func(n string) bool {
return n == cursedKey
})
}
}
memo = make(map[string]map[string]int)
sum := g.dfsUntil("svr", "out", nil)
return sum, nil
}
func main() {
input, err := utils.DownloadTodaysInputFile()
if err != nil {
_ = fmt.Errorf("%v\n", err)
}
utils.RunStep(utils.ONE, input, stepOne)
utils.RunStep(utils.TWO, input, stepTwo)
}
I have also finally written a function to automatically download the input file (which will prove useful for... ehm... tomorrow):
download today's input file
func DownloadTodaysInputFile() (FilePath, error) {
today := time.Now().Day()
url := fmt.Sprintf("https://adventofcode.com/2025/day/%d/input", today)
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return FilePath(""), err
}
// const sessionValue = 'hehehehehehe'
req.Header.Set("Cookie", fmt.Sprintf("session=%s", sessionValue))
resp, err := client.Do(req)
if err != nil {
return FilePath(""), err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return FilePath(""), err
}
path := fmt.Sprintf("day%d.txt", today)
f, err := os.Create(path)
if err != nil {
return FilePath(""), err
}
defer f.Close()
_, err = f.Write(data)
if err != nil {
return FilePath(""), err
}
return FilePath(path), nil
}
Haskell
import Control.Arrow
import Data.Char
import Text.ParserCombinators.ReadP
import Data.Array qualified as A
import Data.Map.Strict qualified as M
parse = M.fromList . fst . last . readP_to_S (((,) <$> (munch1 isAlpha <* string ": ") <*> (munch1 isAlpha `sepBy` char ' ')) `endBy` char '\n')
out = 0 :: Int -- index of out node
buildAdjList m = (keys, adj)
where
keys = M.insert "out" out . snd . M.mapAccumWithKey (\a k _ -> (succ a, a)) (succ out) $ m
adj = A.listArray (out, out + M.size m) $ [] : (fmap (keys M.!) <$> M.elems m)
findPaths adj src dest = go src
where
go i
| i == dest = 1 :: Int
| otherwise = sum $ (r A.!) <$> (adj A.! i)
r = A.listArray bounds $ go <$> A.range bounds
bounds = A.bounds adj
part1 (keys, adj) = findPaths adj (keys M.! "you") out
-- Since graph must be acyclic, one of fft_dac or dac_fft will be 0
part2 (keys, adj)
| fft_dac /= 0 = svr_fft * fft_dac * dac_out
| otherwise = svr_dac * dac_fft * fft_out
where
[svr, fft, dac] = (keys M.!) <$> ["svr", "fft", "dac"]
svr_fft = findPaths adj svr fft
fft_dac = findPaths adj fft dac
dac_out = findPaths adj dac out
svr_dac = findPaths adj svr dac
dac_fft = findPaths adj dac fft
fft_out = findPaths adj fft out
main = getContents >>= print . (part1 &&& part2) . buildAdjList . parse
There’s so many great features hidden behind ‘un’.
I’m sure that part2 could be simplified significantly but I was fed up by then.
Rust
Part 1 was very straight forward. I overcomplicated it a bit by using an actual graph library, but I'm used to using it.
I originally tried to brute force Part 2, which of course failed. I then reverted to dynamic programming, which worked well.
use std::collections::{HashMap, VecDeque};
use graphrs::{Graph, GraphSpecs};
use crate::solver::DaySolver;
pub struct Day11Solver;
impl DaySolver for Day11Solver {
fn part1(&self, input: String) -> String {
let mut graph: Graph<String, ()> = Graph::new(GraphSpecs::directed_create_missing());
let edges = input.lines()
.flat_map(|line| {
let (start, end_str) = line.split_once(": ").unwrap();
end_str.split_whitespace()
.map(|end| (start.to_string(), end.to_string()))
})
.collect();
graph.add_edge_tuples(edges).unwrap();
let mut frontier = VecDeque::from([vec!["you".to_string()]]);
let mut path_count = 0;
while let Some(path) = frontier.pop_front() {
let last = path.last().unwrap();
if last == "out" {
path_count += 1;
} else {
graph
.get_successor_node_names(last.to_string())
.unwrap()
.into_iter()
.filter(|next| !path.contains(next))
.map(|next| {
let mut new_path = path.clone();
new_path.push(next.clone());
new_path
})
.for_each(|new_path| frontier.push_back(new_path));
}
}
path_count.to_string()
}
fn part2(&self, input: String) -> String {
let mut graph: Graph<String, ()> = Graph::new(GraphSpecs::directed_create_missing());
let edges = input.lines()
.flat_map(|line| {
let (start, end_str) = line.split_once(": ").unwrap();
end_str.split_whitespace()
.map(|end| (start.to_string(), end.to_string()))
})
.collect();
graph.add_edge_tuples(edges).unwrap();
how_many_paths(
&mut HashMap::new(),
&graph,
("svr".to_string(), false, false),
)
.to_string()
}
}
fn how_many_paths(
cache: &mut HashMap<(String, bool, bool), usize>,
graph: &Graph<String, ()>,
current: (String, bool, bool),
) -> usize {
if let Some(&c) = cache.get(¤t) {
c
} else {
let (node, has_dac, has_fft) = ¤t;
if node == "out" {
let count = if *has_dac && *has_fft { 1 } else { 0 };
cache.insert(current, count);
count
} else {
let count = graph
.get_successor_node_names(node.clone())
.unwrap()
.into_iter()
.map(|next| {
let next_state = (next.to_string(), *has_dac || next == "dac", *has_fft || next == "fft");
how_many_paths(cache, graph, next_state)
})
.sum();
cache.insert(current, count);
count
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn part1() {
let input = include_str!("../../inputs/test/11");
let solver = Day11Solver {};
assert_eq!("5", solver.part1(input.to_string()));
}
#[test]
fn part2() {
let input = include_str!("../../inputs/test/11-2");
let solver = Day11Solver {};
assert_eq!("2", solver.part2(input.to_string()));
}
}
Uiua
If it's stupid but it works...
My dirty hack
I broke the maze into SVR>FFT>DAC>OUT and SVR>DAC>FFT>OUT, realised that the latter was taking a suspiciously long time, so submitted just the answer from the former --> success!
link (You'll need to up the execution limit)
# AOC2025day11 - mazes.
# Uncomment for Part 1
D ← "aaa: you hhh\nyou: bbb ccc\nbbb: ddd eee\nccc: ddd eee fff\nddd: ggg\neee: out\nfff: out\nggg: out\nhhh: ccc fff iii\niii: out"
# Uncomment for Part 2
# D ← "svr: aaa bbb\naaa: fft\nfft: ccc\nbbb: tty\ntty: ccc\nccc: ddd eee\nddd: hub\nhub: fff\neee: dac\ndac: fff\nfff: ggg hhh\nggg: out\nhhh: out"
# D ← &fras "randomAOC/AOC2025day11.txt"
Tabs ← ⊜(⊙□°⊂⊜∘¬⊸∊": ")⊸≠@\nD
Nexts ← ⍣(°□⊡˜⨂⊙Tabs|[])
Part₁ ← ⊙◌⧻path(˙≠°⊏Nexts|≍"out")"you"
Part₂ ← (
⊙◌⧻path(˙≠°⊏Nexts|≍"fft")"svr"
⊙◌⧻path(˙≠°⊏Nexts|≍"dac")"fft"
⊙◌⧻path(˙≠°⊏Nexts|≍"out")"dac"
××
)
# Only one will be right for the test data, depending on dataset.
⊃Part₁ Part₂
Rust
After the struggles of yesterday, nice to have an easy one. Still havent solved yesterdays pt2, struggling to understand z3 + rust.
Hope everyone else isnt too demoralised by the last 2 days!
spoiler
fn count_paths2(
paths: &HashMap<&str, Vec<&str>>,
seen: &mut HashMap<String, usize>,
node: &str,
dest: &str,
) -> usize {
if let Some(c) = seen.get(node) {
return *c;
}
if node == dest {
seen.insert(node.to_string(), 1);
return 1;
}
let Some(next) = paths.get(node) else {
return 0;
};
let mut total_paths = 0;
for node in next {
total_paths += count_paths2(paths, seen, node, dest)
}
seen.insert(node.to_string(), total_paths);
total_paths
}
#[test]
fn test_y2025_day11_part2() {
let input = include_str!("../../input/2025/day_11.txt");
let mut paths: HashMap<&str, Vec<&str>> = HashMap::new();
input.lines().for_each(|line| {
let (source, dests) = line.split_once(":").unwrap();
let dests = dests.split(" ").collect::<Vec<&str>>();
paths.insert(source, dests);
});
// srv -> fft -> dac -> out
let mut total1 = 1;
{
let mut seen = HashMap::new();
total1 *= count_paths2(&paths, &mut seen, "svr", "fft");
}
{
let mut seen = HashMap::new();
total1 *= count_paths2(&paths, &mut seen, "fft", "dac");
}
{
let mut seen = HashMap::new();
total1 *= count_paths2(&paths, &mut seen, "dac", "out");
}
// srv -> dac -> fft -> out
let mut total2 = 1;
{
let mut seen = HashMap::new();
total2 *= count_paths2(&paths, &mut seen, "svr", "dac");
}
{
let mut seen = HashMap::new();
total2 *= count_paths2(&paths, &mut seen, "dac", "fft");
}
{
let mut seen = HashMap::new();
total2 *= count_paths2(&paths, &mut seen, "fft", "out");
}
println!("total: {}", total1 + total2);
assert_eq!(total1 + total2, 509312913844956);
}
If you work out a solution to yesterday's part 2 which isn't just "cheat and rely on an external solver library", I will be most impressed.
Programming and mathematics have quite a large overlap and if you want to write tough puzzles it'll be "deep in the woods" for both, but that question is very much on the maths side. And "are you able to pass arguments to Z3?" isn't a very satisfying puzzle for me.