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 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
//! # Radioisotope Thermoelectric Generators
//!
//! Solves using a [BFS](https://en.wikipedia.org/wiki/Breadth-first_search) from the
//! starting position where each next state is the possible elevator moves either one floor up or
//! down. This was faster than using [A*](https://en.wikipedia.org/wiki/A*_search_algorithm)
//! with a heuristic.
//!
//! A huge critical optimization is the observation that generators and chips are *fungible*.
//! Only the total number of generators and chips on each floor is important.
//! The rules for a valid floor are either:
//!
//! * Any amount of microchips only with no generators.
//! * The amount of generators is greater than the number of microchips, ensuring that each chip
//! is paired with its generator.
//!
//! This allows us to efficiently memoize previously seen states and reject any that we've seen
//! before extremely quickly. Other optimizations:
//!
//! * If we can move 2 items up, then skip only moving 1 item up.
//! * If we can move 1 item down, then skip moving 2 items down
//! * If floor 1 is empty then don't move items back down to it, similarly if both floor 1 and
//! floor 2 are empty then don't move items to them.
//!
//! As a further optimization we assume that there are no more than 15 generators and microchips
//! and store the total packed into a single byte for each floor. This reduces the size of each
//! state to only 8 bytes making it quick to copy and hash.
use crate::util::hash::*;
use std::collections::VecDeque;
// Interestingly it was slightly faster using a `u32` for `elevator` so that the total size of
// the struct is 8 bytes instead of using a `u8` so that the size is 5 bytes.
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct State {
elevator: u32,
floor: [Floor; 4],
}
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
struct Floor {
both: u8,
}
impl Floor {
// Pack generators into the high nibble and microchips into the low nibble.
#[inline]
fn new(generators: usize, microchips: usize) -> Self {
Floor { both: ((generators << 4) + microchips) as u8 }
}
#[inline]
fn generators(self) -> u8 {
self.both >> 4
}
#[inline]
fn microchips(self) -> u8 {
self.both & 0xf
}
#[inline]
fn total(self) -> u8 {
self.generators() + self.microchips()
}
// Check if we can move the requested number of items from this floor.
#[inline]
fn gte(self, other: Floor) -> bool {
self.generators() >= other.generators() && self.microchips() >= other.microchips()
}
// Criticial optimization treating generators and microchips as fungible.
#[inline]
fn valid(self) -> bool {
self.generators() == 0 || self.generators() >= self.microchips()
}
// Addition and subtraction can be done in parallel for both generators and microchips.
#[inline]
fn add(self, other: Floor) -> Floor {
Floor { both: self.both + other.both }
}
#[inline]
fn sub(self, other: Floor) -> Floor {
Floor { both: self.both - other.both }
}
}
pub fn parse(input: &str) -> State {
// Only the *total* number of generators and microchips on each floor is important.
let mut state = State::default();
for (i, line) in input.lines().enumerate() {
let generators = line.matches("generator").count();
let microchips = line.matches("microchip").count();
state.floor[i] = Floor::new(generators, microchips);
}
state
}
pub fn part1(input: &State) -> u32 {
bfs(*input)
}
pub fn part2(input: &State) -> u32 {
let mut modified = *input;
modified.floor[0] = modified.floor[0].add(Floor::new(2, 2));
bfs(modified)
}
fn bfs(start: State) -> u32 {
// Get the total number of all generator and microchips so we know when done.
let complete = start.floor.iter().map(|&f| f.total()).sum();
// The lift must have a least one item and at most two.
// As an optimization the list is ordered in *descending* order of number of items.
let moves =
[Floor::new(2, 0), Floor::new(1, 1), Floor::new(0, 2), Floor::new(1, 0), Floor::new(0, 1)];
let mut todo = VecDeque::new();
let mut seen = FastSet::with_capacity(30_000);
todo.push_back((start, 0));
seen.insert(start);
while let Some((state, steps)) = todo.pop_front() {
if state.floor[3].total() == complete {
return steps;
}
let current = state.elevator as usize;
// Only move down if it makes sense.
if (state.elevator == 1 && state.floor[0].total() > 0)
|| (state.elevator == 2 && (state.floor[0].total() > 0 || state.floor[1].total() > 0))
|| state.elevator == 3
{
let below = current - 1;
let mut min = 2;
for &delta in moves.iter().rev() {
// If we can move 1 item down then skip moving 2.
if delta.total() > min {
break;
}
// Check we have enough items to move
if state.floor[current].gte(delta) {
let candidate = state.floor[below].add(delta);
// Check the destination floor won't fry any microchips.
if candidate.valid() {
// Compute the next state
let mut next = state;
next.floor[current] = next.floor[current].sub(delta);
next.floor[below] = candidate;
next.elevator -= 1;
// Reject any previously seen states.
if seen.insert(next) {
min = delta.total();
todo.push_back((next, steps + 1));
}
}
}
}
}
if state.elevator < 3 {
let above = current + 1;
let mut max = 0;
for delta in moves {
// If we can move 2 items up then skip moving just 1.
if delta.total() < max {
break;
}
// Check we have enough items to move
if state.floor[current].gte(delta) {
let candidate = state.floor[above].add(delta);
// Check the destination floor won't fry any microchips.
if candidate.valid() {
// Compute the next state
let mut next = state;
next.floor[current] = next.floor[current].sub(delta);
next.floor[above] = candidate;
next.elevator += 1;
// Reject any previously seen states.
if seen.insert(next) {
max = delta.total();
todo.push_back((next, steps + 1));
}
}
}
}
}
}
unreachable!()
}