year2024/
day16.rs

1use std::cmp::Reverse;
2use std::collections::BinaryHeap;
3use utils::grid;
4use utils::prelude::*;
5
6/// Finding the shortest paths through a maze.
7#[derive(Clone, Debug)]
8pub struct Day16 {
9    grid: Vec<u8>,
10    start: usize,
11    end: usize,
12    offsets: [isize; 4],
13    cheapest: Vec<[u32; 4]>,
14    turned: Vec<u8>,
15    part1: u32,
16}
17
18impl Day16 {
19    pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
20        let (rows, cols, mut grid) = grid::from_str(input, |b| match b {
21            b'.' | b'#' | b'S' | b'E' => Some(b),
22            _ => None,
23        })?;
24
25        if !grid::is_enclosed(rows, cols, &grid, |&b| b == b'#') {
26            return Err(InputError::new(
27                input,
28                0,
29                "expected grid to be enclosed by walls",
30            ));
31        }
32
33        let mut starts = grid.iter().enumerate().filter(|&(_, &b)| b == b'S');
34        let Some((start, _)) = starts.next() else {
35            return Err(InputError::new(input, 0, "expected one start"));
36        };
37        if starts.count() > 0 {
38            return Err(InputError::new(input, 0, "expected one start"));
39        }
40        grid[start] = b'.';
41
42        let mut ends = grid.iter().enumerate().filter(|&(_, &b)| b == b'E');
43        let Some((end, _)) = ends.next() else {
44            return Err(InputError::new(input, 0, "expected one end"));
45        };
46        if ends.count() > 0 {
47            return Err(InputError::new(input, 0, "expected one end"));
48        }
49        grid[end] = b'.';
50
51        let mut instance = Self {
52            cheapest: vec![[u32::MAX; 4]; grid.len()],
53            turned: vec![0; grid.len()],
54            part1: 0,
55            grid,
56            start,
57            end,
58            offsets: [1, cols as isize, -1, -(cols as isize)],
59        };
60
61        // Precompute part 1 as dijkstra output is needed for both parts
62        if !instance.dijkstra() {
63            return Err(InputError::new(input, 'E', "no path"));
64        }
65
66        Ok(instance)
67    }
68
69    fn dijkstra(&mut self) -> bool {
70        let mut queue = BinaryHeap::new();
71        queue.push(Reverse((0, self.start, 0)));
72
73        while let Some(Reverse((score, index, dir))) = queue.pop() {
74            if score > self.cheapest[index][dir] {
75                continue;
76            }
77            if index == self.end {
78                self.part1 = score;
79                return true;
80            }
81
82            // Advancing to the next branch each time instead of the neighbor reduces the number
83            // of items pushed to the priority queue significantly
84            if let Some(branch) =
85                self.find_branch(index.wrapping_add_signed(self.offsets[dir]), dir, score + 1)
86            {
87                // Reverse needed to use BinaryHeap as a min heap and order by the lowest score
88                queue.push(Reverse(branch));
89            }
90
91            for next_dir in [(dir + 1) % 4, (dir + 3) % 4] {
92                // Only turn if it will be the cheapest way to reach the turned state
93                if score + 1000 < self.cheapest[index][next_dir] {
94                    self.cheapest[index][next_dir] = score + 1000;
95                    // Store that this state was reached by turning, not by traversing tiles heading
96                    // in the next direction to allow reversing the path correctly
97                    self.turned[index] |= 1 << next_dir;
98
99                    if let Some(branch) = self.find_branch(
100                        index.wrapping_add_signed(self.offsets[next_dir]),
101                        next_dir,
102                        score + 1001,
103                    ) {
104                        queue.push(Reverse(branch));
105                    }
106                }
107            }
108        }
109
110        false
111    }
112
113    fn find_branch(
114        &mut self,
115        mut index: usize,
116        mut dir: usize,
117        mut score: u32,
118    ) -> Option<(u32, usize, usize)> {
119        if self.grid[index] != b'.' {
120            return None;
121        }
122
123        while index != self.end {
124            let mut count = 0;
125            let mut next_index = 0;
126            let mut next_dir = 0;
127            for d in [dir, (dir + 1) % 4, (dir + 3) % 4] {
128                let i = index.wrapping_add_signed(self.offsets[d]);
129                if self.grid[i] == b'.' {
130                    count += 1;
131                    next_index = i;
132                    next_dir = d;
133                }
134            }
135
136            if count == 0 {
137                return None;
138            } else if count > 1 {
139                break;
140            }
141
142            score += if dir == next_dir { 1 } else { 1001 };
143            index = next_index;
144            dir = next_dir;
145        }
146
147        if score <= self.cheapest[index][dir] {
148            self.cheapest[index][dir] = score;
149            self.turned[index] &= !(1 << dir);
150            Some((score, index, dir))
151        } else if score == self.cheapest[index][dir] {
152            // Ensure the new state isn't marked as only reachable by turning, which would prevent
153            // this segment from being included when reversing
154            self.turned[index] &= !(1 << dir);
155            None
156        } else {
157            None
158        }
159    }
160
161    #[must_use]
162    pub fn part1(&self) -> u32 {
163        self.part1
164    }
165
166    #[must_use]
167    pub fn part2(&self) -> u32 {
168        let mut on_best = vec![false; self.grid.len()];
169        on_best[self.start] = true;
170        on_best[self.end] = true;
171        for d in 0..4 {
172            if self.cheapest[self.end][d] == self.part1 {
173                let prev = self.end.wrapping_add_signed(-self.offsets[d]);
174                self.reverse(prev, d, self.part1 - 1, &mut on_best);
175            }
176        }
177        on_best.iter().filter(|&&b| b).count() as u32
178    }
179
180    fn reverse(&self, index: usize, dir: usize, score: u32, on_best: &mut [bool]) {
181        if on_best[index] {
182            return;
183        }
184        on_best[index] = true;
185
186        let mut count = 0;
187        let mut next_index = 0;
188        let mut next_dir = 0;
189        for d in [dir, (dir + 1) % 4, (dir + 3) % 4] {
190            let i = index.wrapping_add_signed(-self.offsets[d]);
191            if self.grid[i] == b'.' {
192                count += 1;
193                next_index = i;
194                next_dir = d;
195            }
196        }
197        assert!(count > 0);
198
199        if count == 1 {
200            let next_score = score - if dir == next_dir { 1 } else { 1001 };
201            self.reverse(next_index, next_dir, next_score, on_best);
202        } else {
203            // At a branch, only continue down directions where the cheapest seen score matches
204            for (next_dir, next_score) in [
205                (dir, score),
206                ((dir + 1) % 4, score - 1000),
207                ((dir + 3) % 4, score - 1000),
208            ] {
209                if self.cheapest[index][next_dir] == next_score
210                    && self.turned[index] & (1 << next_dir) == 0
211                {
212                    self.reverse(
213                        index.wrapping_add_signed(-self.offsets[next_dir]),
214                        next_dir,
215                        next_score - 1,
216                        on_best,
217                    );
218                }
219            }
220        }
221    }
222}
223
224examples!(Day16 -> (u32, u32) [
225    {file: "day16_example0.txt", part1: 7036, part2: 45},
226    {file: "day16_example1.txt", part1: 11048, part2: 64},
227]);