advent_of_code/year2021/
day23.rs

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    // First NUM_ROOMS*(2+SIDE_ROOM_SIZE*2) bits are for rooms. 40 bits max.
23    // Then 7*3=21 bits for hallway.
24    // So for biggest SIDE_ROOM_SIZE we fit in 61 bits.
25    storage: u64,
26}
27
28impl<const SIDE_ROOM_SIZE: usize> SearchState<SIDE_ROOM_SIZE> {
29    /// A room is represented by 10 bits:
30    /// 2 bits counting the occupancy (starting from bottom of room)
31    /// This can only hold 4 values, while the occupancy can range from
32    /// 0 to 4 - that is 5 distinct values. We use:
33    /// 0b11 = 4 amphipods
34    /// 0b10 = 3 amphipods
35    /// 0b01 = 2 amphipods
36    /// 0b00 = 1 OR 0 amphipods
37    ///        To determine, check if all second amphipod (`0b0000_XX00`) are stored with 0:s.
38    ///        As we normally set unoccupied to 1:s when popping
39    ///        that means it is empty.
40    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    /// First a bit if occupied, then amphipod (if occupied)
45    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                // Need to clear all bits, for is_hallway_empty() to work fast.
93                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            // If we set all bits to 0:s, that means it is empty
110            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    /// Returns offset of pushed amphipod.
129    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        // Clear and set storage bits:
141        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        // Set amphipod bits
150        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        // Special hack to differentiate 0 and 1 as room occupancy:
155        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    // Returns (popped_depth, amphipod) if there is an amphipod.
164    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        // Clear and set storage bits:
175        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        // Inspect amphipod to pop:
184        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        // Special hack to differentiate 0 and 1 as room occupancy:
191        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                // No need to consider more moves:
259                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                    // No point in leaving last of owns room. TODO: Generalise, avoid leaving if e.g. two A furthest in?
270                    continue;
271                }
272                for horizontal_direction in [-1, 1] {
273                    // [ H0 H1  H2  H3  H4  H5 H6 ]
274                    //        R0  R1  R2  R3
275                    // When going left from room 0, coming to 1+0+0 = 1
276                    // When going right from room 1, coming to 1+0+1 = 2
277                    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                // If the hallway is empty after we have left the initial position we are done.
317                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}