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
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465


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