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 reuse 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
27/// Parse the input into an adjacency matrix of edges compressed into `u32` bitfields.
28///
29/// First, each cave is assigned a unique index, with `0` reserved for the `start` cave and `1`
30/// reserved for the `end` cave. For example, the sample input caves are:
31///
32/// | start | end | A | b | c | d |
33/// | :---: | :-: | - | - | - | - |
34/// | 0 | 1 | 2 | 3 | 4 | 5 |
35///
36/// Next a `vec` of `u32` with an entry for each cave at the corresponding index is created with
37/// a bit set for each other cave reachable at `2ⁿ` where n is the cave index. The start cave
38/// can only be visited once at the beginning, so it is removed from all edges.
39/// For example, the sample start cave `vec` looks like:
40///
41/// | cave | index | edges |
42/// | ----- | ----- | ------ |
43/// | start | 0 | 1100 |
44/// | end | 1 | 1100 |
45/// | A | 2 | 11010 |
46/// | b | 3 | 100110 |
47/// | c | 4 | 100 |
48/// | d | 5 | 1000 |
49///
50/// Finally, all small caves are added to a single `u32`, for example the
51/// sample data looks like `111011`.
52pub fn parse(input: &str) -> Input {
53 let tokens: Vec<_> =
54 input.split(|c: char| !c.is_ascii_alphabetic()).filter(|s| !s.is_empty()).collect();
55
56 let mut indices = FastMap::build([("start", START), ("end", END)]);
57 for token in &tokens {
58 let next = indices.len();
59 indices.entry(token).or_insert(next);
60 }
61
62 let mut edges = vec![0; indices.len()];
63 for [a, b] in tokens.iter().chunk::<2>() {
64 edges[indices[a]] |= 1 << indices[b];
65 edges[indices[b]] |= 1 << indices[a];
66 }
67 let not_start = !(1 << START);
68 edges.iter_mut().for_each(|edge| *edge &= not_start);
69
70 let small = indices
71 .iter()
72 .filter(|(key, _)| key.as_bytes()[0].is_ascii_lowercase())
73 .fold(0, |small, (_, index)| small | (1 << index));
74
75 Input { small, edges }
76}
77
78/// Explore the cave system visiting all small caves only once.
79pub fn part1(input: &Input) -> u32 {
80 explore(input, false)
81}
82
83/// Explore the cave system visiting a single small cave twice and the other small caves only once.
84pub fn part2(input: &Input) -> u32 {
85 explore(input, true)
86}
87
88/// Convenience method to create initial state.
89fn explore(input: &Input, twice: bool) -> u32 {
90 // Calculate the needed size of the cache as the product of:
91 // * 2 states for boolean "twice".
92 // * n states for the number of caves including start and end.
93 // * 2⁽ⁿ⁻²⁾ states for the possible visited combinations, not including start and end cave.
94 let size = 2 * input.edges.len() * (1 << (input.edges.len() - 2));
95 let mut cache = vec![0; size];
96
97 paths(input, START, 0, twice, &mut cache)
98}
99
100/// Core recursive DFS logic.
101///
102/// First we check if we have either reached the `end` cave or seen this state before,
103/// returning early in either case with the respective result.
104///
105/// Next we use bit manipulation to quickly iterate through the caves connected to our current
106/// location. The [`trailing_zeros`] method returns the next set bit. This intrinsic compiles to
107/// a single machine code instruction on x86 and ARM and is blazing fast. We remove visited caves
108/// using a `^` XOR instruction.
109///
110/// The nuance is reusing the same code for both part one and part two. First we check if we can visit
111/// a cave using the rules for part one. If not, then we also check if the `twice` variable is
112/// still `true`. This variable allows a single second visit to a small cave. The expression
113/// `once && twice` sets this value to `false` whenever we need to use it to visit a small cave.
114///
115/// [`trailing_zeros`]: u32::trailing_zeros
116fn paths(input: &Input, from: usize, visited: u32, twice: bool, cache: &mut [u32]) -> u32 {
117 // Calculate index by converting "twice" to either 1 or 0, then multiplying "from" by 2
118 // (the cardinality of "twice") and "visited" by "edges.len()".
119 // Subtle nuance, by not multiplying "visited" by 2 and also dividing by 2 we ignore the
120 // two least significant bits for start and end cave, as these will always be 0 and 1
121 // respectively.
122 let index = twice as usize + 2 * from + (input.edges.len() * (visited as usize / 2));
123 if cache[index] > 0 {
124 return cache[index];
125 }
126
127 let mut caves = input.edges[from];
128 let mut total = 0;
129 let end = 1 << END;
130
131 if caves & end != 0 {
132 caves ^= end;
133 total += 1;
134 }
135
136 for to in caves.biterator() {
137 let mask = 1 << to;
138 let once = input.small & mask == 0 || visited & mask == 0;
139
140 if once || twice {
141 total += paths(input, to, visited | mask, once && twice, cache);
142 }
143 }
144
145 cache[index] = total;
146 total
147}