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    part1: u32,
15}
16
17impl Day16 {
18    pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
19        let ((_, cols, grid), start, end) = grid::parse_maze(input, 1)?;
20
21        let mut instance = Self {
22            cheapest: vec![[u32::MAX; 4]; grid.len()],
23            part1: 0,
24            grid,
25            start,
26            end,
27            offsets: [1, cols as isize, -1, -(cols as isize)],
28        };
29
30        // Precompute part 1 as dijkstra output is needed for both parts
31        if !instance.dijkstra() {
32            return Err(InputError::new(input, 'E', "no path"));
33        }
34
35        Ok(instance)
36    }
37
38    fn dijkstra(&mut self) -> bool {
39        let mut queue = BinaryHeap::new();
40        queue.push(Reverse((0, self.start, 0)));
41        self.cheapest[self.start][0] = 0;
42
43        while let Some(Reverse((score, index, dir))) = queue.pop() {
44            if score > self.cheapest[index][dir] {
45                continue;
46            }
47            if index == self.end {
48                self.part1 = score;
49                return true;
50            }
51
52            // Advancing to the next branch each time instead of the neighbor reduces the number
53            // of items pushed to the priority queue significantly
54            if let Some(branch) =
55                self.find_branch(index.wrapping_add_signed(self.offsets[dir]), dir, score + 1)
56            {
57                // Reverse needed to use BinaryHeap as a min heap and order by the lowest score
58                queue.push(Reverse(branch));
59            }
60
61            for next_dir in [(dir + 1) % 4, (dir + 3) % 4] {
62                // Only turn if it will be the cheapest way to reach the turned state
63                if score + 1000 < self.cheapest[index][next_dir] {
64                    self.cheapest[index][next_dir] = score + 1000;
65
66                    if let Some(branch) = self.find_branch(
67                        index.wrapping_add_signed(self.offsets[next_dir]),
68                        next_dir,
69                        score + 1001,
70                    ) {
71                        queue.push(Reverse(branch));
72                    }
73                }
74            }
75        }
76
77        false
78    }
79
80    fn find_branch(
81        &mut self,
82        mut index: usize,
83        mut dir: usize,
84        mut score: u32,
85    ) -> Option<(u32, usize, usize)> {
86        if self.grid[index] != b'.' {
87            return None;
88        }
89
90        loop {
91            if score < self.cheapest[index][dir] {
92                self.cheapest[index][dir] = score;
93            } else if score > self.cheapest[index][dir] {
94                return None;
95            }
96
97            if index == self.end {
98                break;
99            }
100
101            let mut count = 0;
102            let mut next_index = 0;
103            let mut next_dir = 0;
104            for d in [dir, (dir + 1) % 4, (dir + 3) % 4] {
105                let i = index.wrapping_add_signed(self.offsets[d]);
106                if self.grid[i] == b'.' {
107                    count += 1;
108                    next_index = i;
109                    next_dir = d;
110                }
111            }
112
113            if count == 0 {
114                return None;
115            } else if count > 1 {
116                break;
117            }
118
119            score += if dir == next_dir { 1 } else { 1001 };
120            index = next_index;
121            dir = next_dir;
122        }
123
124        Some((score, index, dir))
125    }
126
127    #[must_use]
128    pub fn part1(&self) -> u32 {
129        self.part1
130    }
131
132    #[must_use]
133    pub fn part2(&self) -> u32 {
134        let mut on_best = vec![0u8; self.grid.len()];
135        on_best[self.start] = 0b1111;
136        on_best[self.end] = 0b1111;
137        for d in 0..4 {
138            if self.cheapest[self.end][d] == self.part1 {
139                let prev = self.end.wrapping_add_signed(-self.offsets[d]);
140                self.reverse(prev, d, self.part1 - 1, &mut on_best);
141            }
142        }
143        on_best.iter().filter(|&&b| b != 0).count() as u32
144    }
145
146    fn reverse(&self, index: usize, dir: usize, score: u32, on_best: &mut [u8]) {
147        if on_best[index] & (1 << dir) != 0 {
148            return;
149        }
150        on_best[index] |= 1 << dir;
151
152        let mut count = 0;
153        let mut next_index = 0;
154        let mut next_dir = 0;
155        for d in [dir, (dir + 1) % 4, (dir + 3) % 4] {
156            let i = index.wrapping_add_signed(-self.offsets[d]);
157            if self.grid[i] == b'.' {
158                count += 1;
159                next_index = i;
160                next_dir = d;
161            }
162        }
163        assert!(count > 0);
164
165        if count == 1 {
166            let next_score = score - if dir == next_dir { 1 } else { 1001 };
167            self.reverse(next_index, next_dir, next_score, on_best);
168        } else {
169            // At a branch, only continue down directions where the cheapest seen score matches
170            for (next_dir, next_score) in [
171                (dir, score),
172                ((dir + 1) % 4, score - 1000),
173                ((dir + 3) % 4, score - 1000),
174            ] {
175                let next_index = index.wrapping_add_signed(-self.offsets[next_dir]);
176                if self.cheapest[index][next_dir] == next_score
177                    && self.cheapest[next_index][next_dir] == next_score - 1
178                {
179                    self.reverse(next_index, next_dir, next_score - 1, on_best);
180                }
181            }
182        }
183    }
184}
185
186examples!(Day16 -> (u32, u32) [
187    {file: "day16_example0.txt", part1: 7036, part2: 45},
188    {file: "day16_example1.txt", part1: 11048, part2: 64},
189]);