-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathday12.rs
More file actions
159 lines (140 loc) · 5.89 KB
/
day12.rs
File metadata and controls
159 lines (140 loc) · 5.89 KB
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
//! # 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 reuse 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 adjacency 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ⁿ` 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 {
let next = indices.len();
indices.entry(token).or_insert(next);
}
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.as_bytes()[0].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⁽ⁿ⁻²⁾ 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 connected to our current
/// location. The [`trailing_zeros`] method returns the next set bit. This intrinsic 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 reusing the same code for both part one and part two. First we check if we can visit
/// a cave using the rules for part one. 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 = twice as usize + 2 * from + (input.edges.len() * (visited as usize / 2));
if cache[index] > 0 {
return cache[index];
}
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
}