aoc/year2024/
day06.rs

1//! # Guard Gallivant
2//!
3//! Part two is sped up by pre-computing the next obstacle in each direction from any point in
4//! the grid. If there is nothing left in the way then coordinates outside the grid are used.
5//! One dimensional example:
6//!
7//! ```none
8//!     .#...
9//!     Left: (-1, 2, 2, 2, 2)
10//!     Right: (1, 1, 5, 5, 5)
11//! ```
12//!
13//! This allows us to "shortcut" to each obstacle when looking for cycles. The remaining tricky
14//! part is including the extra obstacle which is different for each point on the guard's path.
15//!
16//! The search can be parallelized across multiple threads as each position is independent.
17use crate::util::grid::*;
18use crate::util::hash::*;
19use crate::util::point::*;
20use crate::util::thread::*;
21use std::sync::atomic::{AtomicUsize, Ordering};
22
23pub fn parse(input: &str) -> Grid<u8> {
24    Grid::parse(input)
25}
26
27/// Count distinct positions in the guard's path, which will eventually leave the grid.
28pub fn part1(grid: &Grid<u8>) -> usize {
29    let mut grid = grid.clone();
30    let mut position = grid.find(b'^').unwrap();
31    let mut direction = UP;
32    let mut result = 1;
33
34    while grid.contains(position + direction) {
35        if grid[position + direction] == b'#' {
36            direction = direction.clockwise();
37            continue;
38        }
39
40        let next = position + direction;
41
42        // Avoid double counting when the path crosses itself.
43        if grid[next] == b'.' {
44            result += 1;
45            grid[next] = b'^';
46        }
47
48        position = next;
49    }
50
51    result
52}
53
54/// Follow the guard's path, checking every step for a potential cycle.
55pub fn part2(grid: &Grid<u8>) -> usize {
56    let mut grid = grid.clone();
57    let mut position = grid.find(b'^').unwrap();
58    let mut direction = UP;
59    let mut path = Vec::with_capacity(5_000);
60
61    while grid.contains(position + direction) {
62        if grid[position + direction] == b'#' {
63            direction = direction.clockwise();
64        }
65
66        let next = position + direction;
67
68        // Avoid double counting when the path crosses itself.
69        if grid[next] == b'.' {
70            path.push((position, direction));
71            grid[next] = b'^';
72        }
73
74        position = next;
75    }
76
77    // Use as many cores as possible to parallelize the remaining search.
78    let shortcut = Shortcut::from(&grid);
79    let total = AtomicUsize::new(0);
80
81    spawn_parallel_iterator(&path, |iter| worker(&shortcut, &total, iter));
82    total.into_inner()
83}
84
85fn worker(shortcut: &Shortcut, total: &AtomicUsize, iter: ParIter<'_, (Point, Point)>) {
86    let mut seen = FastSet::new();
87    let result = iter
88        .filter(|(position, direction)| {
89            seen.clear();
90            is_cycle(shortcut, &mut seen, *position, *direction)
91        })
92        .count();
93
94    total.fetch_add(result, Ordering::Relaxed);
95}
96
97fn is_cycle(
98    shortcut: &Shortcut,
99    seen: &mut FastSet<(Point, Point)>,
100    mut position: Point,
101    mut direction: Point,
102) -> bool {
103    let obstacle = position + direction;
104
105    while shortcut.up.contains(position) {
106        // Reaching the same position in the same direction is a cycle.
107        if !seen.insert((position, direction)) {
108            return true;
109        }
110
111        // The tricky part is checking for the new time travelling instigated obstacle.
112        position = match direction {
113            UP => {
114                let next = shortcut.up[position];
115                if position.x == obstacle.x && position.y > obstacle.y && obstacle.y >= next.y {
116                    obstacle - UP
117                } else {
118                    next
119                }
120            }
121            DOWN => {
122                let next = shortcut.down[position];
123                if position.x == obstacle.x && position.y < obstacle.y && obstacle.y <= next.y {
124                    obstacle - DOWN
125                } else {
126                    next
127                }
128            }
129            LEFT => {
130                let next = shortcut.left[position];
131                if position.y == obstacle.y && position.x > obstacle.x && obstacle.x >= next.x {
132                    obstacle - LEFT
133                } else {
134                    next
135                }
136            }
137            RIGHT => {
138                let next = shortcut.right[position];
139                if position.y == obstacle.y && position.x < obstacle.x && obstacle.x <= next.x {
140                    obstacle - RIGHT
141                } else {
142                    next
143                }
144            }
145            _ => unreachable!(),
146        };
147
148        direction = direction.clockwise();
149    }
150
151    false
152}
153
154struct Shortcut {
155    up: Grid<Point>,
156    down: Grid<Point>,
157    left: Grid<Point>,
158    right: Grid<Point>,
159}
160
161impl Shortcut {
162    fn from(grid: &Grid<u8>) -> Self {
163        let mut up = grid.same_size_with(ORIGIN);
164        let mut down = grid.same_size_with(ORIGIN);
165        let mut left = grid.same_size_with(ORIGIN);
166        let mut right = grid.same_size_with(ORIGIN);
167
168        for x in 0..grid.width {
169            let mut last = Point::new(x, -1);
170
171            for y in 0..grid.height {
172                let point = Point::new(x, y);
173                if grid[point] == b'#' {
174                    last = Point::new(x, y + 1);
175                }
176                up[point] = last;
177            }
178        }
179
180        for x in 0..grid.width {
181            let mut last = Point::new(x, grid.height);
182
183            for y in (0..grid.height).rev() {
184                let point = Point::new(x, y);
185                if grid[point] == b'#' {
186                    last = Point::new(x, y - 1);
187                }
188                down[point] = last;
189            }
190        }
191
192        for y in 0..grid.height {
193            let mut last = Point::new(-1, y);
194
195            for x in 0..grid.width {
196                let point = Point::new(x, y);
197                if grid[point] == b'#' {
198                    last = Point::new(x + 1, y);
199                }
200                left[point] = last;
201            }
202        }
203
204        for y in 0..grid.height {
205            let mut last = Point::new(grid.width, y);
206
207            for x in (0..grid.width).rev() {
208                let point = Point::new(x, y);
209                if grid[point] == b'#' {
210                    last = Point::new(x - 1, y);
211                }
212                right[point] = last;
213            }
214        }
215
216        Shortcut { up, down, left, right }
217    }
218}