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