advent_of_code/year2016/
day11.rs

1use crate::input::Input;
2use std::cmp::Ordering;
3use std::cmp::Reverse;
4use std::collections::{BinaryHeap, HashMap, HashSet};
5use std::hash::{Hash, Hasher};
6
7#[derive(Clone, Default, Eq, PartialEq, Hash, Copy)]
8struct Floor {
9    generators: u8,
10    microchips: u8,
11}
12
13impl Floor {
14    fn add_item(&mut self, chip: bool, isotope_id: u8) {
15        let bit_mask = 1 << isotope_id;
16        if chip {
17            self.microchips |= bit_mask;
18        } else {
19            self.generators |= bit_mask;
20        }
21    }
22
23    fn remove_item(&mut self, chip: bool, isotope_id: u8) {
24        let bit_mask = !(1 << isotope_id);
25        if chip {
26            self.microchips &= bit_mask;
27        } else {
28            self.generators &= bit_mask;
29        }
30    }
31
32    const fn is_valid(self) -> bool {
33        let contains_generator = self.generators != 0;
34        let contains_unshielded_microchip = self.microchips & self.generators != self.microchips;
35        !(contains_generator && contains_unshielded_microchip)
36    }
37
38    const fn count_items(self) -> u32 {
39        self.generators.count_ones() + self.microchips.count_ones()
40    }
41}
42
43#[derive(Clone, Eq)]
44struct State {
45    current_floor: i8,
46    floors: [Floor; 4],
47}
48
49impl State {
50    fn pairs(&self) -> usize {
51        let mut result = 0;
52        let mut current_idx = 0;
53        for (floor_idx, floor) in self.floors.iter().enumerate() {
54            for offset in 0..8 {
55                let bit_mask = 1 << offset;
56                if floor.microchips & bit_mask != 0 {
57                    for (match_floor_idx, match_floor) in self.floors.iter().enumerate() {
58                        if match_floor.generators & bit_mask != 0 {
59                            result |=
60                                (floor_idx << current_idx) + (match_floor_idx << (current_idx + 2));
61                            current_idx += 4;
62                        }
63                    }
64                }
65            }
66        }
67        result
68    }
69}
70
71impl Hash for State {
72    fn hash<H: Hasher>(&self, hasher: &mut H) {
73        self.current_floor.hash(hasher);
74        self.pairs().hash(hasher);
75    }
76}
77
78impl PartialEq for State {
79    fn eq(&self, other: &Self) -> bool {
80        self.current_floor == other.current_floor && self.pairs() == other.pairs()
81    }
82}
83
84impl Ord for State {
85    fn cmp(&self, other: &Self) -> Ordering {
86        other.current_floor.cmp(&self.current_floor)
87    }
88}
89
90impl PartialOrd for State {
91    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
92        Some(self.cmp(other))
93    }
94}
95
96fn parse_input(input: &str, part2: bool) -> Result<[Floor; 4], String> {
97    let mut name_to_id = HashMap::new();
98    let mut current_id = 0_u8;
99    let mut initial_floors = [
100        Floor::default(),
101        Floor::default(),
102        Floor::default(),
103        Floor::default(),
104    ];
105
106    for (floor_idx, line) in input.lines().enumerate() {
107        // "The first floor contains a hydrogen-compatible microchip and a lithium-compatible microchip.
108        // The second floor contains a hydrogen generator.
109        // The third floor contains a lithium generator.
110        // The fourth floor contains nothing relevant."
111        let words = line.split(' ').collect::<Vec<_>>();
112        for (word_idx, &word) in words.iter().enumerate() {
113            let (isotope_name, microchip) = if word_idx > 0 && word.starts_with("microchip") {
114                let isotope_name = words[word_idx - 1]
115                    .strip_suffix("-compatible")
116                    .ok_or("Invalid syntax - not $ISOTYPE-compatible before 'microchip'")?;
117                (isotope_name, true)
118            } else if word_idx > 0 && word.starts_with("generator") {
119                let isotope_name = words[word_idx - 1];
120                (isotope_name, false)
121            } else {
122                continue;
123            };
124
125            let isotope_id = *name_to_id
126                .entry(isotope_name.to_string())
127                .or_insert_with(|| {
128                    current_id += 1;
129                    current_id - 1
130                });
131            if isotope_id == 6 {
132                return Err("Too many isotopes - max supported is 5".to_string());
133            }
134            let bit_mask = 1 << isotope_id;
135
136            if microchip {
137                initial_floors[floor_idx].microchips |= bit_mask;
138            } else {
139                initial_floors[floor_idx].generators |= bit_mask;
140            }
141        }
142    }
143
144    if part2 {
145        let elerium_id = current_id + 1;
146        let dilithium_id = current_id + 2;
147        initial_floors[0].add_item(true, elerium_id);
148        initial_floors[0].add_item(false, elerium_id);
149        initial_floors[0].add_item(true, dilithium_id);
150        initial_floors[0].add_item(false, dilithium_id);
151    }
152
153    Ok(initial_floors)
154}
155
156pub fn solve(input: &Input) -> Result<u32, String> {
157    let initial_floors = parse_input(input.text, input.is_part_two())?;
158    let mut to_visit = BinaryHeap::new();
159    let mut visited_states = HashSet::new();
160
161    let initial_state = State {
162        // "When you enter the containment area, you and the elevator will start on the first floor":
163        current_floor: 0,
164        floors: initial_floors,
165    };
166
167    to_visit.push(Reverse((0, 0, initial_state.clone())));
168    visited_states.insert(initial_state);
169
170    while let Some(Reverse((_, visited_state_cost, visited_state))) = to_visit.pop() {
171        if visited_state
172            .floors
173            .iter()
174            .take(3)
175            .all(|floor| floor.count_items() == 0)
176        {
177            // If floor 0-3 is empty we're done.
178            return Ok(visited_state_cost);
179        }
180
181        for direction in [-1, 1] {
182            let new_floor = visited_state.current_floor + direction;
183            if !(0..=3).contains(&new_floor) {
184                continue;
185            }
186            if direction == -1
187                && visited_state
188                    .floors
189                    .iter()
190                    .take(visited_state.current_floor as usize)
191                    .all(|floor| floor.count_items() == 0)
192            {
193                // Do not bring anything down if every floor beneath current is empty.
194                continue;
195            }
196
197            let current_floor = visited_state.floors[visited_state.current_floor as usize];
198            for first_moved_is_chip in [true, false] {
199                for first_offset in 0..8 {
200                    let contains_first_item = if first_moved_is_chip {
201                        current_floor.microchips
202                    } else {
203                        current_floor.generators
204                    } & (1 << first_offset)
205                        != 0;
206                    if !contains_first_item {
207                        continue;
208                    }
209
210                    for &second_moved_is_chip in if first_moved_is_chip {
211                        [true, false].iter()
212                    } else {
213                        [false].iter()
214                    } {
215                        for second_offset in 0..=(if first_moved_is_chip == second_moved_is_chip {
216                            first_offset
217                        } else {
218                            7
219                        }) {
220                            let contains_second_item = if second_moved_is_chip {
221                                current_floor.microchips
222                            } else {
223                                current_floor.generators
224                            } & (1 << second_offset)
225                                != 0;
226                            if !contains_second_item {
227                                continue;
228                            }
229
230                            let mut new_floors = visited_state.floors;
231
232                            new_floors[visited_state.current_floor as usize]
233                                .remove_item(first_moved_is_chip, first_offset);
234                            new_floors[new_floor as usize]
235                                .add_item(first_moved_is_chip, first_offset);
236
237                            if (first_moved_is_chip, first_offset)
238                                != (second_moved_is_chip, second_offset)
239                            {
240                                new_floors[visited_state.current_floor as usize]
241                                    .remove_item(second_moved_is_chip, second_offset);
242                                new_floors[new_floor as usize]
243                                    .add_item(second_moved_is_chip, second_offset);
244                            }
245
246                            if !new_floors.iter().all(|&floor| floor.is_valid()) {
247                                continue;
248                            }
249
250                            let new_cost = visited_state_cost + 1;
251                            let new_state = State {
252                                current_floor: new_floor,
253                                floors: new_floors,
254                            };
255
256                            let do_insert = visited_states.insert(new_state.clone());
257                            if do_insert {
258                                // Encourage moving things up:
259                                let heuristic = (new_state.floors[0].count_items() * 3) / 2
260                                    + new_state.floors[1].count_items()
261                                    + new_state.floors[2].count_items() / 2;
262                                to_visit.push(Reverse((new_cost + heuristic, new_cost, new_state)));
263                            }
264                        }
265                    }
266                }
267            }
268        }
269    }
270
271    Err("No solution found".to_string())
272}
273
274#[test]
275pub fn tests() {
276    use crate::input::{test_part_one, test_part_two};
277
278    let example_input = "The first floor contains a promethium generator and a promethium-compatible microchip.
279The second floor contains a cobalt generator, a curium generator, a ruthenium generator, and a plutonium generator.
280The third floor contains a cobalt-compatible microchip, a curium-compatible microchip, a ruthenium-compatible microchip, and a plutonium-compatible microchip.
281The fourth floor contains nothing relevant.";
282    test_part_one!(example_input => 33);
283    test_part_two!(example_input => 57);
284
285    let real_input = include_str!("day11_input.txt");
286    test_part_one!(real_input => 37);
287    test_part_two!(real_input => 61);
288}