Skip to main content

year2016/
day11.rs

1use std::collections::VecDeque;
2use utils::bit::BitIterator;
3use utils::hash::{FastMap, FastSet};
4use utils::prelude::*;
5
6/// Minimizing steps to safely rearrange generators and microchips.
7///
8/// The key optimization is that states are equivalent if swapping the positions of
9/// generator-microchip pairs would make them equal.
10///
11/// Additionally, work out which generators/microchips are safe to move, instead of wasting time
12/// checking if states are valid.
13#[derive(Clone, Debug)]
14pub struct Day11 {
15    floors: [Floor; 4],
16    types: usize,
17}
18#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
19struct State {
20    floors: [Floor; 4],
21    elevator: u8,
22    steps: u16,
23}
24
25#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
26struct Floor {
27    generators: u8,
28    microchips: u8,
29}
30
31impl Day11 {
32    pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
33        if input.lines().count() != 4 {
34            return Err(InputError::new(input, 0, "expected 4 floors"));
35        }
36
37        let mut floors = [Floor::default(); 4];
38        let mut types = FastMap::new();
39        for (
40            line,
41            Floor {
42                generators,
43                microchips,
44            },
45        ) in input.lines().zip(&mut floors)
46        {
47            for (matches, field) in [
48                (line.match_indices(" generator"), generators),
49                (line.match_indices("-compatible microchip"), microchips),
50            ] {
51                for (index, _) in matches {
52                    let Some((_, type_str)) = line[..index].rsplit_once(' ') else {
53                        return Err(InputError::new(input, index, "expected type"));
54                    };
55
56                    let types_len = types.len();
57                    let type_index = *types.entry(type_str).or_insert(types_len);
58                    if type_index >= 5 {
59                        return Err(InputError::new(input, index, "too many types"));
60                    }
61
62                    *field |= 1 << type_index;
63                }
64            }
65        }
66
67        Ok(Self {
68            floors,
69            types: types.len(),
70        })
71    }
72
73    #[must_use]
74    pub fn part1(&self) -> u16 {
75        Self::minimum_steps(self.floors, self.types)
76    }
77
78    #[must_use]
79    pub fn part2(&self) -> u16 {
80        let mut floors = self.floors;
81        floors[0].generators |= 0b11 << self.types;
82        floors[0].microchips |= 0b11 << self.types;
83        Self::minimum_steps(floors, self.types + 2)
84    }
85
86    fn minimum_steps(floors: [Floor; 4], types: usize) -> u16 {
87        // Ensure the current state is valid, as the code below assumes the current state is always valid
88        if types > 7 {
89            panic!("only 7 types supported"); // An eighth could be supported by updating to_unique
90        }
91        for f in floors {
92            if f.generators != 0 && (f.microchips & !f.generators) != 0 {
93                // Triggered running part 2 on the example input, as it starts with unpaired
94                // microchips on the first floor
95                panic!("invalid start state");
96            }
97        }
98
99        let mut queue = VecDeque::with_capacity(1024);
100        let mut visited = FastSet::with_capacity(10240);
101
102        let start = State {
103            floors,
104            elevator: 0,
105            steps: 0,
106        };
107        queue.push_back(start);
108        visited.insert(start.to_unique());
109
110        let all_types = !(u8::MAX << types);
111        while let Some(state) = queue.pop_front() {
112            if state.floors[3].microchips == all_types && state.floors[3].generators == all_types {
113                return state.steps;
114            }
115
116            let src = state.floors[state.elevator as usize];
117            let src_pairs = src.generators & src.microchips;
118            let src_unpaired_generators = src.generators & !src.microchips;
119
120            // If any generators are moved from the current floor, work out which can be moved and
121            // which must be moved.
122            let (src_gen_can_move, src_gen_must_move) = if src.generators == 0 {
123                // No generators to move
124                (0, 0)
125            } else if src_pairs.count_ones() > 2
126                || (src_pairs.count_ones() == 2 && src_unpaired_generators != 0)
127                || (src_pairs.count_ones() == 1 && src_unpaired_generators.count_ones() >= 2)
128            {
129                // Only possible to move unpaired generators, as moving one of the paired generators
130                // will leave a generator and an incompatible microchip behind
131                (src_unpaired_generators, 0)
132            } else if src_pairs.count_ones() == 2 && src_unpaired_generators == 0 {
133                // Both paired generators must be moved, as leaving one behind will break
134                // the incompatible microchip
135                (src_pairs, src_pairs)
136            } else if src_pairs.count_ones() == 1 && src_unpaired_generators.count_ones() <= 1 {
137                // Unpaired generator must be moved (if present), paired generator can be moved
138                (src.generators, src_unpaired_generators)
139            } else {
140                // All generators can be moved
141                (src.generators, 0)
142            };
143
144            for elevator in [state.elevator + 1, state.elevator.saturating_sub(1)] {
145                if elevator == state.elevator || elevator >= 4 {
146                    continue;
147                }
148
149                // Don't go down to empty flows
150                if elevator < state.elevator
151                    && ((elevator == 0 && state.floors[0].empty())
152                        || (elevator == 1 && state.floors[0].empty() && state.floors[1].empty()))
153                {
154                    continue;
155                }
156
157                let mut try_move = |generators: u8, microchips: u8| {
158                    let mut next_state = state;
159                    next_state.floors[state.elevator as usize].generators &= !generators;
160                    next_state.floors[state.elevator as usize].microchips &= !microchips;
161
162                    next_state.floors[elevator as usize].generators |= generators;
163                    next_state.floors[elevator as usize].microchips |= microchips;
164
165                    next_state.elevator = elevator;
166                    next_state.steps += 1;
167
168                    if visited.insert(next_state.to_unique()) {
169                        queue.push_back(next_state);
170                    }
171                };
172
173                let dst = state.floors[elevator as usize];
174                let dst_unpaired_microchips = dst.microchips & !dst.generators;
175
176                // Try moving a pair
177                if src_pairs != 0 && dst_unpaired_microchips == 0 {
178                    let pair = src_pairs.isolate_lowest_one();
179                    try_move(pair, pair);
180                }
181
182                // Try moving generators
183                if src_gen_can_move != 0 {
184                    let can_move = src_gen_can_move;
185                    let mut must_move = src_gen_must_move;
186                    if dst_unpaired_microchips != 0 {
187                        // Only safe to move generators if also moving generators required to make pairs
188                        must_move |= dst_unpaired_microchips;
189                    }
190
191                    if must_move.count_ones() <= 2 && (must_move & !can_move) == 0 {
192                        if elevator > state.elevator {
193                            // Going up, move as many generators as possible
194                            if must_move.count_ones() == 2
195                                || (must_move.count_ones() == 1 && (can_move & !must_move) == 0)
196                            {
197                                // 2 must-move generators, or 1 must-move generator if there are no other movable generators
198                                try_move(must_move, 0);
199                            } else if must_move.count_ones() == 1 {
200                                // Any combination of the 1 must-move generator + a can-move generator
201                                for (_, g1) in BitIterator::ones(can_move & !must_move) {
202                                    try_move(must_move | g1, 0);
203                                }
204                            } else if can_move.count_ones() == 1 {
205                                // 1 can-move generator only
206                                try_move(can_move, 0);
207                            } else {
208                                // Any combination of 2 can-move generators
209                                for (_, g1) in BitIterator::ones(can_move) {
210                                    for (_, g2) in BitIterator::ones(can_move & (g1 - 1)) {
211                                        try_move(g1 | g2, 0);
212                                    }
213                                }
214                            }
215                        } else {
216                            // Going down, move as few generators as possible
217                            if must_move != 0 {
218                                // Move the must-move generators
219                                try_move(must_move, 0);
220                            } else {
221                                // Any of the can-move generators by itself
222                                for (_, g1) in BitIterator::ones(can_move) {
223                                    try_move(g1, 0);
224                                }
225                            }
226                        }
227                    }
228                }
229
230                // Try moving microchips
231                if src.microchips != 0 {
232                    let mut to_move = src.microchips;
233                    if dst.generators != 0 {
234                        // Only safe to move microchips to make pairs
235                        to_move &= dst.generators;
236                    }
237
238                    if to_move != 0 {
239                        if elevator > state.elevator {
240                            // Going up, move as many microchips as possible
241                            if to_move.count_ones() >= 2 {
242                                // Any combination of 2 microchips
243                                for (_, m1) in BitIterator::ones(to_move) {
244                                    for (_, m2) in BitIterator::ones(to_move & (m1 - 1)) {
245                                        try_move(0, m1 | m2);
246                                    }
247                                }
248                            } else {
249                                // Only 1 microchip to move
250                                try_move(0, to_move);
251                            }
252                        } else {
253                            // Going down, only move one microchip
254                            for (_, m1) in BitIterator::ones(to_move) {
255                                try_move(0, m1);
256                            }
257                        }
258                    }
259                }
260            }
261        }
262
263        panic!("no solution found")
264    }
265}
266
267impl State {
268    #[inline]
269    fn to_unique(self) -> u64 {
270        // States are equivalent if swapping the positions of generator-microchip pairs would make
271        // them equal. Store visited states by converting the position of each pair into a single
272        // byte (top 4 bits for the microchip's floor, bottom 4 bits for the generator's floor),
273        // then sorting the positions.
274        let mut output = [0u8; 8];
275        output[0] = self.elevator;
276
277        for (i, out) in output[1..].iter_mut().enumerate() {
278            for (f, floor) in self.floors.iter().enumerate() {
279                *out |= if floor.generators & (1 << i) != 0 {
280                    1 << f
281                } else {
282                    0
283                };
284                *out |= if floor.microchips & (1 << i) != 0 {
285                    1 << (f + 4)
286                } else {
287                    0
288                };
289            }
290        }
291        output[1..].sort_unstable();
292
293        // Hashing one u64 seems to be faster than hashing 8 or 9 bytes, but as one of the bytes is
294        // needed to store the elevator position, only 7 types are supported.
295        u64::from_ne_bytes(output)
296    }
297}
298
299impl Floor {
300    fn empty(self) -> bool {
301        self.generators == 0 && self.microchips == 0
302    }
303}
304
305examples!(Day11 -> (u16, u16) [
306    {
307        input: "The first floor contains a hydrogen-compatible microchip and a lithium-compatible microchip.\n\
308            The second floor contains a hydrogen generator.\n\
309            The third floor contains a lithium generator.\n\
310            The fourth floor contains nothing relevant.",
311        part1: 11
312    },
313]);