year2016/
day11.rs

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