advent_of_code_2021

My attempts to work through the 2021 Advent of Code problems.
git clone https://git.eamoncaddigan.net/advent_of_code_2021.git
Log | Files | Refs | README | LICENSE

commit 2b670b6d4dd9271ab57fdc292d0c619f62b1a3e0
parent 3164e7220836904bd742529f6ab16fd14d7f9632
Author: Eamon Caddigan <eamon.caddigan@gmail.com>
Date:   Sun, 12 Dec 2021 17:18:18 -0500

Solution to day 12, part 1

Diffstat:
Aday12_part1.py | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 128 insertions(+), 0 deletions(-)

diff --git a/day12_part1.py b/day12_part1.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +"""Advent of Code 2021, day 12 (part 1): Passage Pathing +This problem is graph search, with the wrinkle that "large caves" can be +visited more than once""" + +# We'll find all the paths from `start` to `end` in order to count them. This +# is basically a very straightforward graph traversal, with the caveat that +# "large caves" can be visited more than once--I'm not going to worry about +# getting caught in loops between them (and if I have to change that in code, I +# hope I remember to edit this comment). Since this is my year of pandas and +# numpy, I am going to implement edges with a 2d numpy array (even though the +# edge weights are simple--1 or 0--and symmetrical), and node information with +# 1d numpy arrays. + +from typing import Tuple, List +import numpy as np +from utils import get_puzzle_input + +EXAMPLE_INPUT = \ +"""fs-end +he-DX +fs-he +start-DX +pj-DX +end-zg +zg-sl +zg-pj +pj-he +RW-he +fs-DX +pj-RW +zg-RW +start-pj +he-WI +zg-he +pj-fs +start-RW +""" + +def parse_input(input_string: str) -> Tuple[np.ndarray, np.ndarray]: + """Read the cave map, and return a tuple containing two arrays: + * A 1d array of node names (strings) + * A 2d array (number of nodes x number of nodes) to represent edges""" + # Read in the data and find the nodes and edges + node_set = set() + edge_list = [] + for edge_string in input_string.rstrip('\n').split('\n'): + edge_nodes_set = set(edge_string.split('-')) + node_set.update(edge_nodes_set) + edge_list.append(edge_nodes_set) + + # Create the numpy objects + node_names = np.array(list(node_set)) + edges = np.zeros((node_names.size, node_names.size), dtype=int) + + # Fill in the edges + # (Here's where we're punished for representing the data this way, other + # models would be much easier to parse) + for edge_nodes_set in edge_list: + n_1, n_2 = edge_nodes_set + edges[node_names == n_1, node_names == n_2] = 1 + edges[node_names == n_2, node_names == n_1] = 1 + + return (node_names, edges) + +def is_visitable(node_names: np.ndarray, node_visits: np.ndarray) -> np.ndarray: + """Returns a boolean array indicating which nodes are still visitable + during search (defined as nodes associated with 'large' caverns or nodes + not yet visited)""" + return (node_visits == 0) | np.char.isupper(node_names) + +def update_visits(node_id: int, node_visits: np.ndarray) -> np.ndarray: + """Return a copy of the visit counts with the specified node marked as + visited again""" + node_visits_copy = node_visits.copy() + node_visits_copy[node_id] += 1 + return node_visits_copy + +def traverse_node(node_id: int, + node_names: np.ndarray, + node_visits: np.ndarray, + edges: np.ndarray) -> List[Tuple[int, ...]]: + """Perform depth first search through the given node. Search terminates at + a node that cannot visit any neighbors or a node that's named 'end'. + Returns a list of paths, each path is a tuple of node IDs.""" + # End of search when we hit 'end' + if node_names[node_id] == "end": + return [(node_id,)] + + updated_node_visits = update_visits(node_id, node_visits) + visitable_nodes = (edges[node_id,] > 0) & is_visitable(node_names, updated_node_visits) + + # Also end of search when (if) we hit somebody with no neighbors + if not visitable_nodes.any(): + return[(node_id,)] + + # Continue the search + paths = [] + for neighbor_id in visitable_nodes.nonzero()[0]: + for path in traverse_node(neighbor_id, node_names, updated_node_visits, edges): + paths.append((node_id,) + path) + return paths + +def find_paths_start_to_end(node_names: np.ndarray, + edges: np.ndarray) -> List[Tuple[int, ...]]: + """Return a list of all paths from the node named 'start' to a node named + 'end' (paths are tuples of node IDs)""" + start_node = int((node_names == 'start').nonzero()[0][0]) + end_node = int((node_names == 'end').nonzero()[0][0]) + return [path for path in \ + traverse_node(start_node, + node_names, + np.zeros(node_names.size, dtype=int), + edges) \ + if path[-1] == end_node] + +def solve_puzzle(input_string: str) -> int: + """Return the numeric solution to the puzzle""" + return len(find_paths_start_to_end(*parse_input(input_string))) + +def main() -> None: + """Run when the file is called as a script""" + assert solve_puzzle(EXAMPLE_INPUT) == 226 + print("Number of routes from 'start' to 'end':", + solve_puzzle(get_puzzle_input(12))) + +if __name__ == "__main__": + main()