year2024/
day06.rs

1use utils::geometry::Vec2;
2use utils::grid;
3use utils::prelude::*;
4
5/// Finding obstructions to cause infinite loops.
6#[derive(Clone, Debug)]
7pub struct Day06 {
8    pub rows: usize,
9    pub cols: usize,
10    pub grid: Vec<u8>,
11    pub start: Vec2<usize>,
12}
13
14const DIRECTIONS: [Vec2<isize>; 4] = [Vec2::DOWN, Vec2::RIGHT, Vec2::UP, Vec2::LEFT];
15
16impl Day06 {
17    pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
18        let mut start = None;
19        let (rows, cols, grid) = grid::parse(
20            input,
21            0,
22            0,
23            |b| b,
24            |b| matches!(b, b'.' | b'#'),
25            |i, b| {
26                if b == b'^' && start.is_none() {
27                    start = Some(i);
28                    Ok(b'.')
29                } else if b == b'^' {
30                    Err("expected only one '^'")
31                } else {
32                    Err("expected '.', '#' or '^'")
33                }
34            },
35        )?;
36
37        let Some(start) = start else {
38            return Err(InputError::new(input, 0, "expected one '^'"));
39        };
40
41        Ok(Self {
42            rows,
43            cols,
44            grid,
45            start: Vec2::new(start % cols, start / cols),
46        })
47    }
48
49    #[must_use]
50    pub fn part1(&self) -> usize {
51        let mut pos = self.start;
52        let mut dir = 0;
53        let mut visited = vec![false; self.grid.len()];
54        loop {
55            visited[pos.y * self.cols + pos.x] = true;
56
57            let next = pos.wrapping_add_signed(DIRECTIONS[dir]);
58            if next.x >= self.cols || next.y >= self.rows {
59                break;
60            }
61            if self.grid[next.y * self.cols + next.x] == b'#' {
62                dir = (dir + 1) % 4;
63            } else {
64                pos = next;
65            }
66        }
67        visited.iter().filter(|&&c| c).count()
68    }
69
70    #[must_use]
71    pub fn part2(&self) -> usize {
72        let mut pos = self.start;
73        let mut dir = 0;
74        let mut visited = vec![0u8; self.grid.len()];
75        let mut obstructions = vec![false; self.grid.len()];
76        let mut cached_step_counts = vec![[0; 4]; self.grid.len()];
77        loop {
78            visited[pos.y * self.cols + pos.x] |= 1 << dir;
79
80            let next = pos.wrapping_add_signed(DIRECTIONS[dir]);
81            if next.x >= self.cols || next.y >= self.rows {
82                break;
83            }
84
85            if self.grid[next.y * self.cols + next.x] == b'#' {
86                dir = (dir + 1) % 4;
87            } else {
88                if !obstructions[next.y * self.cols + next.x]
89                    && visited[next.y * self.cols + next.x] == 0
90                    && self.check_cycle(next, pos, dir, &visited, &mut cached_step_counts)
91                {
92                    obstructions[next.y * self.cols + next.x] = true;
93                }
94
95                pos = next;
96            }
97        }
98        obstructions.iter().filter(|&&c| c).count()
99    }
100
101    // Combination of two algorithms starting from the current position:
102    // 1) Checking against previously visited states/if position leaves grid
103    // 2) The start of Brent's algorithm for cycle detection as used in 2017 day 6
104    // This also avoids allocating/zeroing/copying a new visited vec
105    fn check_cycle(
106        &self,
107        obstruction: Vec2<usize>,
108        pos: Vec2<usize>,
109        dir: usize,
110        visited: &[u8],
111        cache: &mut [[isize; 4]],
112    ) -> bool {
113        let (mut power, mut lambda) = (1, 1);
114        let (mut tortoise_pos, mut tortoise_dir) = (pos, dir);
115        let (mut hare_pos, mut hare_dir) = (pos, dir);
116
117        loop {
118            if power == lambda {
119                tortoise_pos = hare_pos;
120                tortoise_dir = hare_dir;
121                power *= 2;
122                lambda = 0;
123            }
124            lambda += 1;
125
126            // Advance to the next obstruction
127            if hare_pos.x == obstruction.x || hare_pos.y == obstruction.y {
128                // On the same X or Y line as the temporary obstruction, loop without caching
129                loop {
130                    let next = hare_pos.wrapping_add_signed(DIRECTIONS[hare_dir]);
131                    if next.x >= self.cols || next.y >= self.rows {
132                        // No cycle, hare has left the grid
133                        return false;
134                    }
135                    if self.grid[next.y * self.cols + next.x] == b'#' || next == obstruction {
136                        break;
137                    }
138                    hare_pos = next;
139                }
140            } else {
141                // Temporary obstruction can be ignored as not on the same X or Y line as it
142                let cached_count = &mut cache[hare_pos.y * self.cols + hare_pos.x][hare_dir];
143                if *cached_count > 0 {
144                    // Advanced by the previously cached count
145                    hare_pos = hare_pos.wrapping_add_signed(DIRECTIONS[hare_dir] * *cached_count);
146                    if hare_pos.x >= self.cols || hare_pos.y >= self.rows {
147                        // No cycle, hare has left the grid
148                        return false;
149                    }
150                } else {
151                    // Loop, caching the step count until the next obstruction
152                    loop {
153                        let next = hare_pos.wrapping_add_signed(DIRECTIONS[hare_dir]);
154                        if next.x >= self.cols || next.y >= self.rows {
155                            // No cycle, hare has left the grid
156                            *cached_count += 1;
157                            return false;
158                        }
159                        if self.grid[next.y * self.cols + next.x] == b'#' {
160                            break;
161                        }
162                        hare_pos = next;
163                        *cached_count += 1;
164                    }
165                }
166            }
167
168            hare_dir = (hare_dir + 1) % 4;
169
170            if visited[hare_pos.y * self.cols + hare_pos.x] & (1 << hare_dir) != 0 {
171                // Cycle, hare has reached a previous state from before adding the obstacle
172                return true;
173            }
174            if hare_pos == tortoise_pos && hare_dir == tortoise_dir {
175                // Cycle, hare and tortoise are in the same state
176                return true;
177            }
178        }
179    }
180}
181
182examples!(Day06 -> (usize, usize) [
183    {file: "day06_example0.txt", part1: 41, part2: 6},
184]);