aoc/year2021/
day12.rs

1//! # Passage Pathing
2//!
3//! Our basic approach is a [DFS](https://en.wikipedia.org/wiki/Depth-first_search) through the cave
4//! system, exploring all possible permutations of the paths and finishing whenever we reach
5//! the `end` cave.
6//!
7//! To speed things up, 2 strategies are used, one high level and one low level:
8//! * [Memoization](https://en.wikipedia.org/wiki/Memoization) (or caching) of the possible paths
9//!   from each position, taking into account previously visited caves is the high level strategy
10//!   to re-use work and save time.
11//! * [Bit Manipulation](https://en.wikipedia.org/wiki/Bit_manipulation) to store both the graph of
12//!   cave connections as an [adjacency matrix](https://en.wikipedia.org/wiki/Adjacency_matrix)
13//!   and the list of visited caves compressed into a single `u32` is the low level strategy to
14//!   quickly and efficiently store the small cardinality set of caves.
15use crate::util::bitset::*;
16use crate::util::hash::*;
17use crate::util::iter::*;
18
19const START: usize = 0;
20const END: usize = 1;
21
22pub struct Input {
23    small: u32,
24    edges: Vec<u32>,
25}
26
27struct State {
28    from: usize,
29    visited: u32,
30    twice: bool,
31}
32
33/// Parse the input into an adjency matrix of edges compressed into `u32` bitfields.
34///
35/// First, each cave is assigned a unique index, with `0` reserved for the `start` cave and `1`
36/// reserved for the `end` cave. For example the sample input caves are:
37///
38/// | start | end | A | b | c | d |
39/// | :---: | :-: | - | - | - | - |
40/// |   0   |  1  | 2 | 3 | 4 | 5 |
41///
42/// Next a `vec` of `u32` with an entry for each cave at the corresponding index is created with
43/// a bit set for each other cave reachable at `2^n` where n is the cave index. The start cave
44/// can only be visited once at the beginning, so it is removed from all edges.
45/// For example the sample start cave `vec` looks like:
46///
47/// | cave  | index | edges  |
48/// | ----- | ----- | ------ |
49/// | start | 0     |   1100 |
50/// | end   | 1     |   1100 |
51/// | A     | 2     |  11010 |
52/// | b     | 3     | 100110 |
53/// | c     | 4     |    100 |
54/// | d     | 5     |   1000 |
55///
56/// Finally all small caves are added to a single `u32`, for example the
57/// sample data looks like `111011`.
58pub fn parse(input: &str) -> Input {
59    let tokens: Vec<_> =
60        input.split(|c: char| !c.is_ascii_alphabetic()).filter(|s| !s.is_empty()).collect();
61
62    let mut indices = FastMap::build([("start", START), ("end", END)]);
63    for token in &tokens {
64        if !indices.contains_key(token) {
65            indices.insert(token, indices.len());
66        }
67    }
68
69    let mut edges = vec![0; indices.len()];
70    for [a, b] in tokens.iter().chunk::<2>() {
71        edges[indices[a]] |= 1 << indices[b];
72        edges[indices[b]] |= 1 << indices[a];
73    }
74    let not_start = !(1 << START);
75    edges.iter_mut().for_each(|edge| *edge &= not_start);
76
77    let mut small = 0;
78    for (key, value) in &indices {
79        if key.chars().next().unwrap().is_ascii_lowercase() {
80            small |= 1 << value;
81        }
82    }
83
84    Input { small, edges }
85}
86
87/// Explore the cave system visiting all small caves only once.
88pub fn part1(input: &Input) -> u32 {
89    explore(input, false)
90}
91
92/// Explore the cave system visiting a single small cave twice and the other small caves only once.
93pub fn part2(input: &Input) -> u32 {
94    explore(input, true)
95}
96
97/// Convenience method to create initial state.
98fn explore(input: &Input, twice: bool) -> u32 {
99    // Calculate the needed size of the cache as the product of:
100    // * 2 states for boolean "twice".
101    // * n states for the number of caves including start and end.
102    // * 2^(n-2) states for the possible visited combinations, not including start and end cave.
103    let size = 2 * input.edges.len() * (1 << (input.edges.len() - 2));
104    let mut cache = vec![0; size];
105
106    let state = State { from: START, visited: 0, twice };
107    paths(input, &state, &mut cache)
108}
109
110/// Core recursive DFS logic.
111///
112/// First we check if we have either reached the `end` cave or seen this state before,
113/// returning early in either case with the respective result.
114///
115/// Next we use bit manipulation to quickly iterate through the caves connnected to our current
116/// location. The [`trailing_zeros`] method returns the next set bit. This instrinsic compiles to
117/// a single machine code instruction on x86 and ARM and is blazing fast. We remove visited caves
118/// using a `^` XOR instruction.
119///
120/// The nuance is re-using the same code for both part 1 and part 2. First we check if we can visit
121/// a cave using the rules for part 1. If not, then we also check if the `twice` variable is
122/// still `true`. This variable allows a single second visit to a small cave. The expression
123/// `once && twice` sets this value to `false` whenever we need to use it to visit a small cave.
124///
125/// [`trailing_zeros`]: u32::trailing_zeros
126fn paths(input: &Input, state: &State, cache: &mut [u32]) -> u32 {
127    let State { from, visited, twice } = *state;
128
129    // Calculate index by converting "twice" to either 1 or 0, then multiplying "from" by 2
130    // (the cardinality of "twice") and "visited" by "edges.len()".
131    // Subtle nuance, by not multiplying "visited" by 2 and also dividing by 2 we ignore the
132    // two least significant bits for start and end cave, as these will always be 0 and 1
133    // respectively.
134    let index =
135        state.twice as usize + 2 * (state.from) + (input.edges.len() * (visited as usize / 2));
136    let total = cache[index];
137    if total > 0 {
138        return total;
139    }
140
141    let mut caves = input.edges[from];
142    let mut total = 0;
143    let end = 1 << END;
144
145    if caves & end != 0 {
146        caves ^= end;
147        total += 1;
148    }
149
150    for to in caves.biterator() {
151        let mask = 1 << to;
152        let once = input.small & mask == 0 || visited & mask == 0;
153
154        if once || twice {
155            let next = State { from: to, visited: visited | mask, twice: once && twice };
156            total += paths(input, &next, cache);
157        }
158    }
159
160    cache[index] = total;
161    total
162}