Skip to content

Commit af249d3

Browse files
committed
Year 2024 Day 6
1 parent ef4b192 commit af249d3

File tree

7 files changed

+257
-0
lines changed

7 files changed

+257
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ Performance is reasonable even on older hardware, for example a 2011 MacBook Pro
8282
| 3 | [Mull It Over](https://adventofcode.com/2024/day/3) | [Source](src/year2024/day03.rs) | 8 |
8383
| 4 | [Ceres Search](https://adventofcode.com/2024/day/4) | [Source](src/year2024/day04.rs) | 77 |
8484
| 5 | [Print Queue](https://adventofcode.com/2024/day/5) | [Source](src/year2024/day05.rs) | 18 |
85+
| 6 | [Guard Gallivant](https://adventofcode.com/2024/day/6) | [Source](src/year2024/day06.rs) | 386 |
8586

8687
## 2023
8788

benches/benchmark.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,5 @@ mod year2024 {
297297
benchmark!(year2024, day03);
298298
benchmark!(year2024, day04);
299299
benchmark!(year2024, day05);
300+
benchmark!(year2024, day06);
300301
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,4 +296,5 @@ pub mod year2024 {
296296
pub mod day03;
297297
pub mod day04;
298298
pub mod day05;
299+
pub mod day06;
299300
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,5 +366,6 @@ fn year2024() -> Vec<Solution> {
366366
solution!(year2024, day03),
367367
solution!(year2024, day04),
368368
solution!(year2024, day05),
369+
solution!(year2024, day06),
369370
]
370371
}

src/year2024/day06.rs

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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.
17+
use crate::util::grid::*;
18+
use crate::util::hash::*;
19+
use crate::util::point::*;
20+
use crate::util::thread::*;
21+
use std::sync::atomic::{AtomicUsize, Ordering};
22+
23+
pub 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.
28+
pub 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.
55+
pub 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_batches(path, |batch| worker(&shortcut, &total, &batch));
82+
total.into_inner()
83+
}
84+
85+
fn worker(shortcut: &Shortcut, total: &AtomicUsize, batch: &[(Point, Point)]) {
86+
let mut seen = FastSet::new();
87+
let result = batch
88+
.iter()
89+
.filter(|(position, direction)| {
90+
seen.clear();
91+
is_cycle(shortcut, &mut seen, *position, *direction)
92+
})
93+
.count();
94+
95+
total.fetch_add(result, Ordering::Relaxed);
96+
}
97+
98+
fn is_cycle(
99+
shortcut: &Shortcut,
100+
seen: &mut FastSet<(Point, Point)>,
101+
mut position: Point,
102+
mut direction: Point,
103+
) -> bool {
104+
let obstacle = position + direction;
105+
106+
while shortcut.up.contains(position) {
107+
// Reaching the same position in the same direction is a cycle.
108+
if !seen.insert((position, direction)) {
109+
return true;
110+
}
111+
112+
// The tricky part is checking for the new time travelling instigated obstacle.
113+
position = match direction {
114+
UP => {
115+
let next = shortcut.up[position];
116+
if position.x == obstacle.x && position.y > obstacle.y && obstacle.y >= next.y {
117+
obstacle - UP
118+
} else {
119+
next
120+
}
121+
}
122+
DOWN => {
123+
let next = shortcut.down[position];
124+
if position.x == obstacle.x && position.y < obstacle.y && obstacle.y <= next.y {
125+
obstacle - DOWN
126+
} else {
127+
next
128+
}
129+
}
130+
LEFT => {
131+
let next = shortcut.left[position];
132+
if position.y == obstacle.y && position.x > obstacle.x && obstacle.x >= next.x {
133+
obstacle - LEFT
134+
} else {
135+
next
136+
}
137+
}
138+
RIGHT => {
139+
let next = shortcut.right[position];
140+
if position.y == obstacle.y && position.x < obstacle.x && obstacle.x <= next.x {
141+
obstacle - RIGHT
142+
} else {
143+
next
144+
}
145+
}
146+
_ => unreachable!(),
147+
};
148+
149+
direction = direction.clockwise();
150+
}
151+
152+
false
153+
}
154+
155+
struct Shortcut {
156+
up: Grid<Point>,
157+
down: Grid<Point>,
158+
left: Grid<Point>,
159+
right: Grid<Point>,
160+
}
161+
162+
impl Shortcut {
163+
fn from(grid: &Grid<u8>) -> Self {
164+
let mut up = copy(grid);
165+
let mut down = copy(grid);
166+
let mut left = copy(grid);
167+
let mut right = copy(grid);
168+
169+
for x in 0..grid.width {
170+
let mut last = Point::new(x, -1);
171+
172+
for y in 0..grid.height {
173+
let point = Point::new(x, y);
174+
if grid[point] == b'#' {
175+
last = Point::new(x, y + 1);
176+
}
177+
up[point] = last;
178+
}
179+
}
180+
181+
for x in 0..grid.width {
182+
let mut last = Point::new(x, grid.height);
183+
184+
for y in (0..grid.height).rev() {
185+
let point = Point::new(x, y);
186+
if grid[point] == b'#' {
187+
last = Point::new(x, y - 1);
188+
}
189+
down[point] = last;
190+
}
191+
}
192+
193+
for y in 0..grid.height {
194+
let mut last = Point::new(-1, y);
195+
196+
for x in 0..grid.width {
197+
let point = Point::new(x, y);
198+
if grid[point] == b'#' {
199+
last = Point::new(x + 1, y);
200+
}
201+
left[point] = last;
202+
}
203+
}
204+
205+
for y in 0..grid.height {
206+
let mut last = Point::new(grid.width, y);
207+
208+
for x in (0..grid.width).rev() {
209+
let point = Point::new(x, y);
210+
if grid[point] == b'#' {
211+
last = Point::new(x - 1, y);
212+
}
213+
right[point] = last;
214+
}
215+
}
216+
217+
Shortcut { up, down, left, right }
218+
}
219+
}
220+
221+
fn copy(grid: &Grid<u8>) -> Grid<Point> {
222+
Grid {
223+
width: grid.width,
224+
height: grid.height,
225+
bytes: vec![ORIGIN; (grid.width * grid.height) as usize],
226+
}
227+
}

tests/test.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,5 @@ mod year2024 {
286286
mod day03_test;
287287
mod day04_test;
288288
mod day05_test;
289+
mod day06_test;
289290
}

tests/year2024/day06_test.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use aoc::year2024::day06::*;
2+
3+
const EXAMPLE: &str = "\
4+
....#.....
5+
.........#
6+
..........
7+
..#.......
8+
.......#..
9+
..........
10+
.#..^.....
11+
........#.
12+
#.........
13+
......#...";
14+
15+
#[test]
16+
fn part1_test() {
17+
let input = parse(EXAMPLE);
18+
assert_eq!(part1(&input), 41);
19+
}
20+
21+
#[test]
22+
fn part2_test() {
23+
let input = parse(EXAMPLE);
24+
assert_eq!(part2(&input), 6);
25+
}

0 commit comments

Comments
 (0)