this post was submitted on 11 Dec 2025
16 points (100.0% liked)

Advent Of Code

1199 readers
1 users here now

An unofficial home for the advent of code community on programming.dev! Other challenges are also welcome!

Advent of Code is an annual Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like.

Everybody Codes is another collection of programming puzzles with seasonal events.

EC 2025

AoC 2025

Solution Threads

M T W T F S S
1 2 3 4 5 6 7
8 9 10 11 12

Visualisations Megathread

Rules/Guidelines

Relevant Communities

Relevant Links

Credits

Icon base by Lorc under CC BY 3.0 with modifications to add a gradient

console.log('Hello World')

founded 2 years ago
MODERATORS
 

Day 11: Reactor

Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL

FAQ

top 19 comments
sorted by: hot top controversial new old
[–] Pyro@programming.dev 3 points 1 week ago* (last edited 1 week ago)

Python

Part 1 can be done with simple dfs / backtracking, even without memoization.

Part 2 needed to have dp / memoization to finish in a reasonable time. Initially, I also checked for cycles in the path, but I removed the check once I realized there were none. Also, instead of adding booleans / int to my memoization function, I chose to compute both path arrangements (svr -> fft -> dac -> out AND svr -> dac -> fft -> out) and add them up.

click to view code

from collections import defaultdict

# build the adjacency graph from the input data
def build_graph(data: str):
    rules_data = data.splitlines()
    graph = defaultdict(list)
    for rule_data in rules_data:
        from_node, to_nodes = rule_data.split(": ")
        graph[from_node].extend(to_nodes.split(' '))
    return graph

# simply dfs every path from 'you' until we reach 'out'
# this is what I initially used for part 1
def count_paths_dfs(data: str, start = "you", end = "out"):
    graph = build_graph(data)

    paths_to_end = 0
    stack = [start]     # currently active paths
    while stack:
        node = stack.pop()
        if node == end:
            # we reached end, no need to check further
            paths_to_end += 1
            continue
        # every node connected to current node is a potential path
        stack.extend(graph[node])
    
    return paths_to_end

# memoized version of counting paths from start to end
def count_paths_memo(graph: defaultdict[list], start = "you", end = "out"):
    # I used to have some code to check for cycles, but thankfully it wasn't needed

    # its pretty much same logic as count_paths_dfs, except recursive and with memoization
    memo = {}
    def backtrack(curr: str):
        if curr in memo:
            return memo[curr]        
        if curr == end:
            ans = 1
        else:
            ans = sum(backtrack(conn) for conn in graph[curr])
        memo[curr] = ans
        return ans
    
    return backtrack(start)

# Optimized part 1 using memoization (not necessary, but faster)
def part1_with_memoiz(data: str, start = "you", end = "out"):
    graph = build_graph(data)
    return count_paths_memo(graph, start, end)

# Part 2 requires counting paths from "svr" to "out", 
#   including only those paths that go through both "fft" and "dac"
# Instead of adding booleans / int to my memoization function, 
#   I chose to compute all path arrangements and add them up
def part2(data: str):
    graph = build_graph(data)

    # getting from start to one requirement
    svr_to_fft = count_paths_memo(graph, "svr", "fft")
    svr_to_dac = count_paths_memo(graph, "svr", "dac")
    # getting from one requirement to the other
    fft_to_dac = count_paths_memo(graph, "fft", "dac")
    dac_to_fft = count_paths_memo(graph, "dac", "fft")
    # the final push to out
    fft_to_out = count_paths_memo(graph, "fft", "out")
    dac_to_out = count_paths_memo(graph, "dac", "out")

    # total paths = (start -> fft -> dac -> out) + (start -> dac -> fft -> out)
    svr_to_out = (
        svr_to_dac * dac_to_fft * fft_to_out +
        svr_to_fft * fft_to_dac * dac_to_out
    )
    return svr_to_out

sample_p1 = """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"""
assert (t := count_paths_dfs(sample_p1)) == 5, f"Expected 5, got {t}"
assert (t := part1_with_memoiz(sample_p1)) == 5, f"Expected 5, got {t}"

sample_p2 = """svr: aaa bbb
aaa: fft
fft: ccc
bbb: tty
tty: ccc
ccc: ddd eee
ddd: hub
hub: fff
eee: dac
dac: fff
fff: ggg hhh
ggg: out
hhh: out"""
assert (t := part2(sample_p2)) == 2, f"Expected 2, got {t}"

[–] cabhan@discuss.tchncs.de 3 points 1 week ago

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(&current) {
        c
    } else {
        let (node, has_dac, has_fft) = &current;
        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()));
    }
}
[–] LeixB@lemmy.world 3 points 1 week ago

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
[–] mykl@lemmy.world 2 points 1 week ago* (last edited 1 week ago) (1 children)

Uiua

If it's stupid but it works...

My dirty hackI 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₂
[–] CameronDev@programming.dev 2 points 1 week ago (2 children)

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.

[–] EnEnCode@programming.dev 2 points 1 week ago (1 children)

If you can explain why it works, not calculating half the paths isn't a hack—it's an optimization. ;)

[–] CameronDev@programming.dev 1 points 1 week ago

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.

[–] mykl@lemmy.world 2 points 1 week ago

Yes, it felt a little more like I was solving the puzzle-setter rather than the puzzle today.

[–] lwhjp@piefed.blahaj.zone 2 points 1 week ago* (last edited 1 week ago) (1 children)

Haskell

Oh, this one was easy (dynamic programming at last!). Still haven't figured out the right way to approach yesterday's part two, though.

import Data.List  
import Data.Map (Map)  
import Data.Map qualified as Map  

readInput =  
  Map.fromList  
    . map ((\(name : outs) -> (init name, outs)) . words)  
    . lines  

part1 input = go "you"  
  where  
    go "out" = 1  
    go name = maybe 0 (sum . map go) $ input Map.!? name  

part2 input = let (both, _, _, _) = pathsFrom "svr" in both  
  where  
    pathsFrom =  
      (Map.!)  
        . Map.insert "out" (0, 0, 0, 1)  
        . Map.fromList  
        . (zip <*> map findPaths)  
        $ Map.keys input ++ concat (Map.elems input)  
    findPaths n =  
      let (both, dac, fft, none) =  
            unzip4 $ maybe [] (map pathsFrom) (input Map.!? n)  
       in case n of  
            "dac" -> (sum both + sum fft, sum dac + sum none, 0, 0)  
            "fft" -> (sum both + sum dac, 0, sum fft + sum none, 0)  
            _ -> (sum both, sum dac, sum fft, sum none)  

main = do  
  input <- readInput <$> readFile "input11"  
  print $ part1 input  
  print $ part2 input  
[–] addie@feddit.uk 1 points 1 week ago (1 children)

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.

[–] Avicenna@programming.dev 2 points 1 week ago

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

[–] addie@feddit.uk 2 points 1 week ago

C++

Dynamic programming and recursion - it's the AoC classic. Nothing tricky here, which is a relief after yesterday.

No sign of Dijkstra's yet, which is a surprise. Maybe it'll be tomorrow's puzzle? 25th is usually an open goal if you've done the rest of the puzzles, but I don't know if that applies to the new shorter format.

#include <boost/log/trivial.hpp>
#include <boost/unordered/unordered_flat_map.hpp>
#include <boost/unordered/unordered_flat_set.hpp>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <vector>

namespace {

using Map =
    boost::unordered::unordered_flat_map<std::string, std::vector<std::string>>;

auto read(const std::string &name) {
  auto rval = Map{};
  auto ih = std::ifstream{name};
  if (!ih)
    throw std::runtime_error{"file?"};
  auto line = std::string{};
  while (std::getline(ih, line)) {
    auto ss = std::istringstream{line};
    auto tmp = std::string{};
    ss >> tmp;
    auto key = tmp.substr(0, tmp.size() - 1);
    auto value = std::vector<std::string>{};
    while (ss >> tmp)
      value.push_back(std::move(tmp));
    rval[key] = std::move(value);
  }
  return rval;
}

auto part1() {
  auto map = read("11.1.txt");
  auto current = std::vector<std::vector<std::string>>{};
  auto paths = boost::unordered::unordered_flat_set<std::vector<std::string>>{};

  current.push_back(std::vector<std::string>{"you"});

  while (!current.empty()) {
    auto next_up = decltype(current){};
    std::swap(current, next_up);
    for (const auto &next : next_up) {
      auto &outputs = map.at(next.back());
      for (auto &output : outputs) {
        if (output == "you")
          continue;
        auto token = next;
        token.push_back(output);
        if (output == "out") {
          paths.insert(std::move(token));
        } else {
          current.push_back(std::move(token));
        }
      }
    }
  }

  return paths.size();
}

struct Route {
  std::string current;
  bool has_fft, has_dac;
};
struct RouteHash {
  std::size_t operator()(const Route &p) const {
    std::size_t seed = 0;
    boost::hash_combine(seed, p.current);
    boost::hash_combine(seed, p.has_fft);
    boost::hash_combine(seed, p.has_dac);
    return seed;
  }
};
auto operator==(const Route &a, const Route &b) {
  return a.current == b.current && a.has_fft == b.has_fft &&
         a.has_dac == b.has_dac;
}
auto cache = boost::unordered::unordered_flat_map<Route, size_t, RouteHash>{};

auto count_target(Route start, const Map &map) -> size_t {
  auto outputs = map.at(start.current);
  if (start.current == "dac")
    start.has_dac = true;
  if (start.current == "fft")
    start.has_fft = true;

  auto rval = size_t{};
  for (auto &output : outputs) {
    if (auto f = cache.find({output, start.has_fft, start.has_dac});
        f != cache.end()) {
      rval += f->second;
      continue;
    }
    if (output == "out") {
      if (start.has_dac && start.has_fft) {
        ++rval;
      }
      continue;
    }
    rval += count_target({output, start.has_fft, start.has_dac}, map);
  }
  cache[start] = rval;
  return rval;
}

auto part2() {
  auto map = read("11.2.txt");
  return count_target({"svr", false, false}, map);
}

} // namespace

auto main() -> int {
  BOOST_LOG_TRIVIAL(info) << "Day 11";
  BOOST_LOG_TRIVIAL(info) << "1: " << part1();
  BOOST_LOG_TRIVIAL(info) << "2: " << part2();
}
[–] CameronDev@programming.dev 2 points 1 week ago (1 children)

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);
    }

[–] EnEnCode@programming.dev 2 points 1 week ago (1 children)

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.

[–] CameronDev@programming.dev 1 points 1 week ago* (last edited 1 week ago)

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

[–] Deebster@programming.dev 2 points 1 week ago* (last edited 1 week ago)

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
}
[–] chunkystyles@sopuli.xyz 2 points 1 week ago* (last edited 1 week ago)

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()
    }
[–] Avicenna@programming.dev 2 points 1 week ago

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

[–] EnEnCode@programming.dev 1 points 1 week ago

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