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}