1use std::cmp::Reverse;
2use std::collections::hash_map::Entry;
3use std::collections::BinaryHeap;
4use std::collections::HashMap;
5use std::fmt::{Debug, Formatter, Write};
6
7use Amphipod::{Amber, Bronze, Copper, Desert};
8
9use crate::input::Input;
10
11pub fn solve(input: &Input) -> Result<u64, String> {
12 (if input.is_part_one() {
13 SearchState::<2>::parse(input.text)?.least_total_energy_to_organize()
14 } else {
15 SearchState::<4>::parse(input.text)?.least_total_energy_to_organize()
16 })
17 .ok_or_else(|| "No solution found".to_string())
18}
19
20#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Default)]
21struct SearchState<const SIDE_ROOM_SIZE: usize> {
22 storage: u64,
26}
27
28impl<const SIDE_ROOM_SIZE: usize> SearchState<SIDE_ROOM_SIZE> {
29 pub const NUM_ROOMS: usize = 4;
41 pub const BITS_PER_ROOM: usize = 2 + SIDE_ROOM_SIZE * 2;
42 pub const ALL_ROOM_BITS: usize = Self::NUM_ROOMS * Self::BITS_PER_ROOM;
43 pub const HALLWAY_SPACES: usize = 7;
44 pub const BITS_PER_HALLWAY: usize = 3;
46
47 fn parse(text: &str) -> Result<Self, String> {
48 let mut result = Self { storage: 0 };
49 let mut amphipod_count = 0;
50 let mut per_type_count = [0_u32; 4];
51 for b in text.bytes().filter(|b| b'A' <= *b && *b <= b'D').rev() {
52 let amphipod_idx = b - b'A';
53 per_type_count[usize::from(amphipod_idx)] += 1;
54 if per_type_count[usize::from(amphipod_idx)] > 2 {
55 return Err(format!("Too many amphipods of type {}", char::from(b)));
56 }
57 result.push_to_room(3 - amphipod_count % 4, Amphipod::from_idx(amphipod_idx));
58 amphipod_count += 1;
59 if SIDE_ROOM_SIZE == 4 && amphipod_count == 4 {
60 for b in [b'D', b'C', b'B', b'A', b'D', b'B', b'A', b'C']
61 .iter()
62 .rev()
63 {
64 result.push_to_room(3 - amphipod_count % 4, Amphipod::from_idx(b - b'A'));
65 amphipod_count += 1;
66 }
67 }
68 }
69 if per_type_count != [2, 2, 2, 2] {
70 return Err("Not 2 of each amphipod type".to_string());
71 }
72 Ok(result)
73 }
74
75 fn get_at_hallway(self, hallway_idx: usize) -> Option<Amphipod> {
76 debug_assert!(
77 hallway_idx < Self::HALLWAY_SPACES,
78 "Hallway idx={hallway_idx}"
79 );
80 let bit_idx = Self::ALL_ROOM_BITS + Self::BITS_PER_HALLWAY * hallway_idx;
81 if self.storage & (1 << (bit_idx + 2)) == 0 {
82 return None;
83 }
84 Some(Amphipod::from_idx(((self.storage >> bit_idx) & 0b11) as u8))
85 }
86
87 fn set_at_hallway(&mut self, hallway_idx: usize, amphipod: Option<Amphipod>) {
88 debug_assert!(hallway_idx <= 6, "Max hallway_idx is 6 (was {hallway_idx})");
89 let bit_idx = Self::ALL_ROOM_BITS + Self::BITS_PER_HALLWAY * hallway_idx;
90 match amphipod {
91 None => {
92 self.storage &= !(0b111 << bit_idx);
94 }
95 Some(a) => {
96 self.storage |= 1 << (bit_idx + 2);
97 self.storage &= !(0b11 << bit_idx);
98 self.storage |= (a as u64) << bit_idx;
99 }
100 }
101 }
102
103 const fn occupancy_in_room(self, room_idx: u8) -> u8 {
104 let room_bit_idx = Self::BITS_PER_ROOM * (room_idx as usize);
105 let room_len_bit_idx = room_bit_idx + SIDE_ROOM_SIZE * 2;
106
107 let amphipods_in_room_storage = ((self.storage >> room_len_bit_idx) & 0b11) as u8;
108 if amphipods_in_room_storage == 0 && ((self.storage >> room_bit_idx) & 0b0000_1100) == 0 {
109 0
111 } else {
112 amphipods_in_room_storage + 1
113 }
114 }
115
116 const fn get_at_room(self, room_idx: u8, room_depth: u8) -> Option<Amphipod> {
117 let amphipods_in_room = self.occupancy_in_room(room_idx);
118 if room_depth >= amphipods_in_room {
119 None
120 } else {
121 let bit_idx = Self::BITS_PER_ROOM as u8 * room_idx;
122 Some(Amphipod::from_idx(
123 (self.storage >> (bit_idx + room_depth * 2) & 0b11) as u8,
124 ))
125 }
126 }
127
128 fn push_to_room(&mut self, room_idx: u8, amphipod: Amphipod) -> u8 {
130 let room_bit_idx = Self::BITS_PER_ROOM * (room_idx as usize);
131 let room_len_bit_idx = room_bit_idx + SIDE_ROOM_SIZE * 2;
132
133 let amphipods_initially_in_room = self.occupancy_in_room(room_idx);
134 debug_assert!(
135 amphipods_initially_in_room < SIDE_ROOM_SIZE as u8,
136 "Cannot push to full side room"
137 );
138 let new_amphipods_in_room = amphipods_initially_in_room + 1;
139
140 let amphipods_in_room_storage = if new_amphipods_in_room == 0 {
142 0
143 } else {
144 new_amphipods_in_room - 1
145 };
146 self.storage &= !(0b11 << room_len_bit_idx);
147 self.storage |= u64::from(amphipods_in_room_storage) << room_len_bit_idx;
148
149 let amphipod_bit_idx = room_bit_idx + 2 * (amphipods_initially_in_room as usize);
151 self.storage &= !(0b11 << amphipod_bit_idx);
152 self.storage |= (amphipod as u64) << amphipod_bit_idx;
153
154 if new_amphipods_in_room == 1 {
156 self.storage |= 0b11 << (amphipod_bit_idx + 2);
157 }
158
159 debug_assert_eq!(self.occupancy_in_room(room_idx), new_amphipods_in_room);
160 SIDE_ROOM_SIZE as u8 - new_amphipods_in_room
161 }
162
163 fn pop_room(&mut self, room_idx: u8) -> Option<(u8, Amphipod)> {
165 let room_bit_idx = Self::BITS_PER_ROOM * (room_idx as usize);
166 let room_len_bit_idx = room_bit_idx + SIDE_ROOM_SIZE * 2;
167 let amphipods_in_room = self.occupancy_in_room(room_idx);
168
169 if amphipods_in_room == 0 {
170 return None;
171 }
172 let new_amphipods_in_room = amphipods_in_room - 1;
173
174 let amphipods_in_room_storage = if new_amphipods_in_room == 0 {
176 0
177 } else {
178 new_amphipods_in_room - 1
179 };
180 self.storage &= !(0b11 << room_len_bit_idx);
181 self.storage |= u64::from(amphipods_in_room_storage) << room_len_bit_idx;
182
183 let amphipod_bit_idx = room_bit_idx + 2 * (new_amphipods_in_room as usize);
185 let result = Some((
186 SIDE_ROOM_SIZE as u8 - amphipods_in_room,
187 Amphipod::from_idx(((self.storage >> amphipod_bit_idx) & 0b11) as u8),
188 ));
189
190 if new_amphipods_in_room == 0 {
192 self.storage &= !(0b11 << (amphipod_bit_idx + 2));
193 } else if new_amphipods_in_room == 1 {
194 self.storage |= 0b11 << (amphipod_bit_idx);
195 }
196
197 debug_assert_eq!(self.occupancy_in_room(room_idx), new_amphipods_in_room);
198
199 result
200 }
201
202 const fn is_hallway_empty(self) -> bool {
203 self.storage >> Self::ALL_ROOM_BITS == 0
204 }
205
206 fn can_amphipod_go_home(self, amphipod: Amphipod) -> bool {
207 let room_idx = amphipod as u8;
208 let occupancy = self.occupancy_in_room(room_idx);
209 if occupancy == 0 {
210 return true;
211 }
212 for i in 0..occupancy {
213 if let Some(amphipod_in_room) = self.get_at_room(amphipod as u8, i) {
214 if amphipod != amphipod_in_room {
215 return false;
216 }
217 }
218 }
219 true
220 }
221
222 fn enumerate_possible_moves(self, moves: &mut Vec<(u64, Self)>) {
223 'hallway_loop: for hallway_idx in 0..Self::HALLWAY_SPACES {
224 if let Some(amphipod) = self.get_at_hallway(hallway_idx) {
225 if !self.can_amphipod_go_home(amphipod) {
226 continue;
227 }
228 let (end_idx, direction) = if (amphipod as usize) + 1 < hallway_idx {
229 ((amphipod as usize) + 2, -1)
230 } else {
231 ((amphipod as usize) + 1, 1)
232 };
233 let mut current_hallway_idx = hallway_idx;
234 let mut hallway_travel_distance = 1;
235 while current_hallway_idx != end_idx {
236 let from_hallway_idx = current_hallway_idx;
237 current_hallway_idx = ((current_hallway_idx as i32) + direction) as usize;
238 if self.get_at_hallway(current_hallway_idx).is_some() {
239 continue 'hallway_loop;
240 }
241
242 hallway_travel_distance += 1;
243 if !matches!(
244 (from_hallway_idx, current_hallway_idx),
245 (0, 1) | (1, 0) | (5, 6) | (6, 5)
246 ) {
247 hallway_travel_distance += 1;
248 }
249 }
250
251 let mut new_state = self;
252 new_state.set_at_hallway(hallway_idx, None);
253 let offset_in_room = new_state.push_to_room(amphipod as u8, amphipod);
254 let total_travel_cost = ((offset_in_room as usize + 1 + hallway_travel_distance)
255 * amphipod.consumption() as usize)
256 as u64;
257 moves.push((total_travel_cost, new_state));
258 return;
260 }
261 }
262
263 for room_idx in 0..Self::NUM_ROOMS {
264 let mut new_state_from_leaving_room = self;
265 if let Some((offset_in_room, amphipod)) =
266 new_state_from_leaving_room.pop_room(room_idx as u8)
267 {
268 if amphipod as usize == room_idx && self.can_amphipod_go_home(amphipod) {
269 continue;
271 }
272 for horizontal_direction in [-1, 1] {
273 let mut hallway_end_idx = 1 + room_idx + usize::from(horizontal_direction == 1);
278 let mut hallway_travel_distance = 1;
279
280 while self.get_at_hallway(hallway_end_idx).is_none() {
281 let mut new_state = new_state_from_leaving_room;
282 new_state.set_at_hallway(hallway_end_idx, Some(amphipod));
283 let total_travel_cost =
284 (1 + u64::from(offset_in_room) + hallway_travel_distance as u64)
285 * u64::from(amphipod.consumption());
286 moves.push((total_travel_cost, new_state));
287
288 let from_hallway_idx = hallway_end_idx;
289 let hallway_end_idx_signed = hallway_end_idx as i32 + horizontal_direction;
290 if hallway_end_idx_signed < 0
291 || hallway_end_idx_signed >= Self::HALLWAY_SPACES as i32
292 {
293 break;
294 }
295 hallway_end_idx = hallway_end_idx_signed as usize;
296 hallway_travel_distance += 1;
297 if !matches!(
298 (from_hallway_idx, hallway_end_idx),
299 (0, 1) | (1, 0) | (5, 6) | (6, 5)
300 ) {
301 hallway_travel_distance += 1;
302 }
303 }
304 }
305 }
306 }
307 }
308
309 fn least_total_energy_to_organize(self) -> Option<u64> {
310 let mut to_visit = BinaryHeap::from([(Reverse(0), self)]);
311 let mut lowest_cost: HashMap<Self, u64> = HashMap::from([(self, 0)]);
312 let mut new_states = Vec::new();
313
314 while let Some((Reverse(cost), state)) = to_visit.pop() {
315 if cost > 0 && state.is_hallway_empty() {
316 return Some(cost);
318 }
319
320 new_states.clear();
321 state.enumerate_possible_moves(&mut new_states);
322 for &(new_state_cost_diff, new_state) in new_states.iter() {
323 let new_total_cost = cost + new_state_cost_diff;
324
325 let visit_this = match lowest_cost.entry(new_state) {
326 Entry::Vacant(entry) => {
327 entry.insert(new_total_cost);
328 true
329 }
330 Entry::Occupied(mut entry) if new_total_cost < *entry.get() => {
331 entry.insert(new_total_cost);
332 true
333 }
334 _ => false,
335 };
336 if visit_this {
337 to_visit.push((Reverse(new_total_cost), new_state));
338 }
339 }
340 }
341
342 None
343 }
344}
345
346const fn to_char(a: Option<Amphipod>) -> char {
347 match a {
348 Some(amphipod) => ((amphipod as u8) + b'A') as char,
349 None => '.',
350 }
351}
352
353impl<const SIDE_ROOM_SIZE: usize> Debug for SearchState<SIDE_ROOM_SIZE> {
354 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
355 f.write_str("#############\n")?;
356 writeln!(
357 f,
358 "#{}{}.{}.{}.{}.{}{}#",
359 to_char(self.get_at_hallway(0)),
360 to_char(self.get_at_hallway(1)),
361 to_char(self.get_at_hallway(2)),
362 to_char(self.get_at_hallway(3)),
363 to_char(self.get_at_hallway(4)),
364 to_char(self.get_at_hallway(5)),
365 to_char(self.get_at_hallway(6)),
366 )?;
367 for side_room_offset in 0..SIDE_ROOM_SIZE {
368 let start_and_end = if side_room_offset == 0 { "##" } else { " " };
369 writeln!(
370 f,
371 "{}#{}#{}#{}#{}#{}",
372 start_and_end,
373 to_char(self.get_at_room(0, SIDE_ROOM_SIZE as u8 - 1 - side_room_offset as u8)),
374 to_char(self.get_at_room(1, SIDE_ROOM_SIZE as u8 - 1 - side_room_offset as u8)),
375 to_char(self.get_at_room(2, SIDE_ROOM_SIZE as u8 - 1 - side_room_offset as u8)),
376 to_char(self.get_at_room(3, SIDE_ROOM_SIZE as u8 - 1 - side_room_offset as u8)),
377 start_and_end
378 )?;
379 }
380 write!(f, " ######### ")
381 }
382}
383
384#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
385enum Amphipod {
386 Amber = 0,
387 Bronze,
388 Copper,
389 Desert,
390}
391
392impl Debug for Amphipod {
393 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
394 let c = (*self as u8) + b'A';
395 f.write_char(c as char)
396 }
397}
398
399impl Amphipod {
400 const fn from_idx(idx: u8) -> Self {
401 match idx {
402 0 => Amber,
403 1 => Bronze,
404 2 => Copper,
405 _ => Desert,
406 }
407 }
408}
409
410impl Amphipod {
411 const fn consumption(self) -> u16 {
412 match self {
413 Amber => 1,
414 Bronze => 10,
415 Copper => 100,
416 Desert => 1000,
417 }
418 }
419}
420
421#[test]
422pub fn tests() {
423 use crate::input::{test_part_one, test_part_two};
424
425 let example = "#############
426#...........#
427###B#C#B#D###
428 #A#D#C#A#
429 #########";
430 test_part_one!(example => 12521);
431 test_part_two!(example => 44169);
432
433 let real_input = include_str!("day23_input.txt");
434 test_part_one!(real_input => 14_460);
435 test_part_two!(real_input => 41_366);
436}
437
438#[test]
439pub fn test_search_state() {
440 let mut s = SearchState::<4>::default();
441 assert!(s.is_hallway_empty());
442 for i in 0..7 {
443 assert_eq!(s.get_at_hallway(i), None);
444 }
445 for i in 0..7 {
446 s.set_at_hallway(i, Some(Amphipod::from_idx((i % 4) as u8)));
447 assert!(!s.is_hallway_empty());
448 for j in 0..7 {
449 if j <= i {
450 assert_eq!(s.get_at_hallway(j), Some(Amphipod::from_idx((j % 4) as u8)));
451 } else {
452 assert_eq!(s.get_at_hallway(j), None);
453 }
454 }
455 }
456
457 for i in 0..7 {
458 s.set_at_hallway(i, None);
459 assert_eq!(None, s.get_at_hallway(i));
460 assert_eq!(i == 6, s.is_hallway_empty());
461 }
462
463 for room_idx in 0..=3 {
464 assert_eq!(s.occupancy_in_room(room_idx), 0);
465 for room_depth in 0..=3 {
466 assert_eq!(s.get_at_room(room_idx, room_depth), None);
467 }
468 }
469
470 for room_idx in 0..=3 {
471 assert_eq!(s.occupancy_in_room(room_idx), 0);
472 for room_depth in 0..=3 {
473 assert_eq!(s.get_at_room(room_idx, room_depth), None);
474 }
475 }
476
477 for room_idx in 0..4 {
478 for amphipod_num in 0..4 {
479 let pushed_amphipod = Amphipod::from_idx(amphipod_num % 4);
480 s.push_to_room(room_idx, pushed_amphipod);
481 assert_eq!(Some(pushed_amphipod), s.get_at_room(room_idx, amphipod_num));
482 assert_eq!(s.occupancy_in_room(room_idx), amphipod_num + 1);
483 }
484 }
485}
486
487#[test]
488pub fn test_search_state_push_pop() {
489 let mut s = SearchState::<4>::default();
490 for room_idx in 0..4 {
491 for amphipod_num in 0..4 {
492 let pushed_amphipod = Amphipod::from_idx(amphipod_num % 4);
493 assert_eq!(3 - amphipod_num, s.push_to_room(room_idx, pushed_amphipod));
494 }
495 for amphipod_num in 0..4 {
496 let pushed_amphipod = Amphipod::from_idx(amphipod_num % 4);
497 assert_eq!(Some(pushed_amphipod), s.get_at_room(room_idx, amphipod_num));
498 }
499 for amphipod_num in (0..4).rev() {
500 let expected = Amphipod::from_idx(amphipod_num % 4);
501 let actual = s.pop_room(room_idx);
502 assert_eq!(Some((3 - amphipod_num, expected)), actual);
503 assert_eq!(amphipod_num, s.occupancy_in_room(room_idx));
504 }
505 }
506}