advent_of_code/year2016/
day11.rs1use 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 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 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 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 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 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}