aoc/year2016/day11.rs
1//! # Radioisotope Thermoelectric Generators
2//!
3//! Solves using a [BFS](https://en.wikipedia.org/wiki/Breadth-first_search) from the
4//! starting position where each next state is the possible elevator moves either one floor up or
5//! down. This was faster than using [A*](https://en.wikipedia.org/wiki/A*_search_algorithm)
6//! with a heuristic.
7//!
8//! A huge critical optimization is the observation that generators and chips are *fungible*.
9//! Only the total number of generators and chips on each floor is important.
10//! The rules for a valid floor are either:
11//!
12//! * Any amount of microchips only with no generators.
13//! * The amount of generators is greater than the number of microchips, ensuring that each chip
14//! is paired with its generator.
15//!
16//! This allows us to efficiently memoize previously seen states and reject any that we've seen
17//! before extremely quickly. Other optimizations:
18//!
19//! * If we can move 2 items up, then skip only moving 1 item up.
20//! * If we can move 1 item down, then skip moving 2 items down
21//! * If floor 1 is empty then don't move items back down to it, similarly if both floor 1 and
22//! floor 2 are empty then don't move items to them.
23//!
24//! As a further optimization we assume that there are no more than 15 generators and microchips
25//! and store the total packed into a single byte for each floor. This reduces the size of each
26//! state to only 8 bytes making it quick to copy and hash.
27use crate::util::hash::*;
28use std::collections::VecDeque;
29
30// Interestingly it was slightly faster using a `u32` for `elevator` so that the total size of
31// the struct is 8 bytes instead of using a `u8` so that the size is 5 bytes.
32#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
33pub struct State {
34 elevator: u32,
35 floor: [Floor; 4],
36}
37
38#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
39struct Floor {
40 both: u8,
41}
42
43impl Floor {
44 // Pack generators into the high nibble and microchips into the low nibble.
45 #[inline]
46 fn new(generators: usize, microchips: usize) -> Self {
47 Floor { both: ((generators << 4) + microchips) as u8 }
48 }
49
50 #[inline]
51 fn generators(self) -> u8 {
52 self.both >> 4
53 }
54
55 #[inline]
56 fn microchips(self) -> u8 {
57 self.both & 0xf
58 }
59
60 #[inline]
61 fn total(self) -> u8 {
62 self.generators() + self.microchips()
63 }
64
65 // Check if we can move the requested number of items from this floor.
66 #[inline]
67 fn gte(self, other: Floor) -> bool {
68 self.generators() >= other.generators() && self.microchips() >= other.microchips()
69 }
70
71 // Criticial optimization treating generators and microchips as fungible.
72 #[inline]
73 fn valid(self) -> bool {
74 self.generators() == 0 || self.generators() >= self.microchips()
75 }
76
77 // Addition and subtraction can be done in parallel for both generators and microchips.
78 #[inline]
79 fn add(self, other: Floor) -> Floor {
80 Floor { both: self.both + other.both }
81 }
82
83 #[inline]
84 fn sub(self, other: Floor) -> Floor {
85 Floor { both: self.both - other.both }
86 }
87}
88
89pub fn parse(input: &str) -> State {
90 // Only the *total* number of generators and microchips on each floor is important.
91 let mut state = State::default();
92
93 for (i, line) in input.lines().enumerate() {
94 let generators = line.matches("generator").count();
95 let microchips = line.matches("microchip").count();
96 state.floor[i] = Floor::new(generators, microchips);
97 }
98
99 state
100}
101
102pub fn part1(input: &State) -> u32 {
103 bfs(*input)
104}
105
106pub fn part2(input: &State) -> u32 {
107 let mut modified = *input;
108 modified.floor[0] = modified.floor[0].add(Floor::new(2, 2));
109 bfs(modified)
110}
111
112fn bfs(start: State) -> u32 {
113 // Get the total number of all generator and microchips so we know when done.
114 let complete = start.floor.iter().map(|&f| f.total()).sum();
115 // The lift must have a least one item and at most two.
116 // As an optimization the list is ordered in *descending* order of number of items.
117 let moves =
118 [Floor::new(2, 0), Floor::new(1, 1), Floor::new(0, 2), Floor::new(1, 0), Floor::new(0, 1)];
119
120 let mut todo = VecDeque::new();
121 let mut seen = FastSet::with_capacity(30_000);
122
123 todo.push_back((start, 0));
124 seen.insert(start);
125
126 while let Some((state, steps)) = todo.pop_front() {
127 if state.floor[3].total() == complete {
128 return steps;
129 }
130
131 let current = state.elevator as usize;
132
133 // Only move down if it makes sense.
134 if (state.elevator == 1 && state.floor[0].total() > 0)
135 || (state.elevator == 2 && (state.floor[0].total() > 0 || state.floor[1].total() > 0))
136 || state.elevator == 3
137 {
138 let below = current - 1;
139 let mut min = 2;
140
141 for &delta in moves.iter().rev() {
142 // If we can move 1 item down then skip moving 2.
143 if delta.total() > min {
144 break;
145 }
146
147 // Check we have enough items to move
148 if state.floor[current].gte(delta) {
149 let candidate = state.floor[below].add(delta);
150
151 // Check the destination floor won't fry any microchips.
152 if candidate.valid() {
153 // Compute the next state
154 let mut next = state;
155 next.floor[current] = next.floor[current].sub(delta);
156 next.floor[below] = candidate;
157 next.elevator -= 1;
158
159 // Reject any previously seen states.
160 if seen.insert(next) {
161 min = delta.total();
162 todo.push_back((next, steps + 1));
163 }
164 }
165 }
166 }
167 }
168
169 if state.elevator < 3 {
170 let above = current + 1;
171 let mut max = 0;
172
173 for delta in moves {
174 // If we can move 2 items up then skip moving just 1.
175 if delta.total() < max {
176 break;
177 }
178
179 // Check we have enough items to move
180 if state.floor[current].gte(delta) {
181 let candidate = state.floor[above].add(delta);
182
183 // Check the destination floor won't fry any microchips.
184 if candidate.valid() {
185 // Compute the next state
186 let mut next = state;
187 next.floor[current] = next.floor[current].sub(delta);
188 next.floor[above] = candidate;
189 next.elevator += 1;
190
191 // Reject any previously seen states.
192 if seen.insert(next) {
193 max = delta.total();
194 todo.push_back((next, steps + 1));
195 }
196 }
197 }
198 }
199 }
200 }
201
202 unreachable!()
203}