Skip to main content

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::*;
21
22struct Shortcut {
23    up: Grid<Point>,
24    down: Grid<Point>,
25    left: Grid<Point>,
26    right: Grid<Point>,
27}
28
29impl Shortcut {
30    fn from(grid: &Grid<u8>) -> Self {
31        let mut up = grid.same_size_with(ORIGIN);
32        let mut down = grid.same_size_with(ORIGIN);
33        let mut left = grid.same_size_with(ORIGIN);
34        let mut right = grid.same_size_with(ORIGIN);
35
36        // Scan each row or column *against* the direction of travel, remembering the square just
37        // before the most recent obstacle. Starting one square off the grid means that
38        // coordinates outside the grid are used when nothing is in the way.
39        let scan = |dst: &mut Grid<Point>, start: Point, step: Point, count: i32| {
40            let mut last = start - step;
41            let mut point = start;
42
43            for _ in 0..count {
44                if grid[point] == b'#' {
45                    last = point + step;
46                }
47                dst[point] = last;
48                point += step;
49            }
50        };
51
52        // Process columns for up/down.
53        for x in 0..grid.width {
54            scan(&mut up, Point::new(x, 0), DOWN, grid.height);
55            scan(&mut down, Point::new(x, grid.height - 1), UP, grid.height);
56        }
57
58        // Process rows for left/right.
59        for y in 0..grid.height {
60            scan(&mut left, Point::new(0, y), RIGHT, grid.width);
61            scan(&mut right, Point::new(grid.width - 1, y), LEFT, grid.width);
62        }
63
64        Self { up, down, left, right }
65    }
66}
67
68pub fn parse(input: &str) -> Grid<u8> {
69    Grid::parse(input)
70}
71
72/// Count distinct positions in the guard's path, which will eventually leave the grid.
73pub fn part1(grid: &Grid<u8>) -> usize {
74    let mut grid = grid.clone();
75    let mut position = grid.find(b'^').unwrap();
76    let mut direction = UP;
77    let mut result = 1;
78
79    while grid.contains(position + direction) {
80        if grid[position + direction] == b'#' {
81            direction = direction.clockwise();
82            continue;
83        }
84
85        let next = position + direction;
86
87        // Avoid double counting when the path crosses itself.
88        if grid[next] == b'.' {
89            result += 1;
90            grid[next] = b'^';
91        }
92
93        position = next;
94    }
95
96    result
97}
98
99/// Follow the guard's path, checking every step for a potential cycle.
100pub fn part2(grid: &Grid<u8>) -> usize {
101    let mut grid = grid.clone();
102    let mut position = grid.find(b'^').unwrap();
103    let mut direction = UP;
104    let mut path = Vec::with_capacity(5_000);
105
106    while grid.contains(position + direction) {
107        if grid[position + direction] == b'#' {
108            direction = direction.clockwise();
109        }
110
111        let next = position + direction;
112
113        // Avoid double counting when the path crosses itself.
114        if grid[next] == b'.' {
115            path.push((position, direction));
116            grid[next] = b'^';
117        }
118
119        position = next;
120    }
121
122    // Use as many cores as possible to parallelize the remaining search.
123    let shortcut = Shortcut::from(&grid);
124    let result = spawn_parallel_iterator(&path, |iter| worker(&shortcut, iter));
125    result.into_iter().sum()
126}
127
128fn worker(shortcut: &Shortcut, iter: ParIter<'_, (Point, Point)>) -> usize {
129    let mut seen = FastSet::new();
130    iter.filter(|&&(position, direction)| {
131        seen.clear();
132        is_cycle(shortcut, &mut seen, position, direction)
133    })
134    .count()
135}
136
137fn is_cycle(
138    shortcut: &Shortcut,
139    seen: &mut FastSet<(Point, Point)>,
140    mut position: Point,
141    mut direction: Point,
142) -> bool {
143    let obstacle = position + direction;
144
145    while shortcut.up.contains(position) {
146        // Reaching the same position in the same direction is a cycle.
147        if !seen.insert((position, direction)) {
148            return true;
149        }
150
151        // The tricky part is checking for the newly introduced time-traveling obstacle.
152        position = match direction {
153            UP => {
154                let next = shortcut.up[position];
155                if position.x == obstacle.x && position.y > obstacle.y && obstacle.y >= next.y {
156                    obstacle - UP
157                } else {
158                    next
159                }
160            }
161            DOWN => {
162                let next = shortcut.down[position];
163                if position.x == obstacle.x && position.y < obstacle.y && obstacle.y <= next.y {
164                    obstacle - DOWN
165                } else {
166                    next
167                }
168            }
169            LEFT => {
170                let next = shortcut.left[position];
171                if position.y == obstacle.y && position.x > obstacle.x && obstacle.x >= next.x {
172                    obstacle - LEFT
173                } else {
174                    next
175                }
176            }
177            RIGHT => {
178                let next = shortcut.right[position];
179                if position.y == obstacle.y && position.x < obstacle.x && obstacle.x <= next.x {
180                    obstacle - RIGHT
181                } else {
182                    next
183                }
184            }
185            _ => unreachable!(),
186        };
187
188        direction = direction.clockwise();
189    }
190
191    false
192}