Skip to content

Commit 8b65706

Browse files
committed
Year 2024 Day 6
1 parent ef4b192 commit 8b65706

File tree

7 files changed

+238
-0
lines changed

7 files changed

+238
-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) | 913 |
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: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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+
use crate::util::grid::*;
16+
use crate::util::hash::*;
17+
use crate::util::point::*;
18+
19+
pub fn parse(input: &str) -> Grid<u8> {
20+
Grid::parse(input)
21+
}
22+
23+
/// Count distinct positions in the guard's path, which will eventually leave the grid.
24+
pub fn part1(grid: &Grid<u8>) -> u32 {
25+
let mut grid = grid.clone();
26+
let mut position = grid.find(b'^').unwrap();
27+
let mut direction = UP;
28+
let mut result = 1;
29+
30+
while grid.contains(position + direction) {
31+
if grid[position + direction] == b'#' {
32+
direction = direction.clockwise();
33+
continue;
34+
}
35+
position += direction;
36+
37+
// Avoid double counting when the path crosses itself.
38+
if grid[position] == b'.' {
39+
result += 1;
40+
}
41+
grid[position] = b'^';
42+
}
43+
44+
result
45+
}
46+
47+
/// Follow the guard's path, checking every step for a potential cycle.
48+
pub fn part2(grid: &Grid<u8>) -> u32 {
49+
let mut grid = grid.clone();
50+
let mut position = grid.find(b'^').unwrap();
51+
let mut direction = UP;
52+
let mut result = 0;
53+
54+
let shortcut = Shortcut::from(&grid);
55+
let mut seen = FastSet::new();
56+
57+
while grid.contains(position + direction) {
58+
if grid[position + direction] == b'#' {
59+
direction = direction.clockwise();
60+
}
61+
62+
let obstacle = position + direction;
63+
64+
// Avoid double counting when the path crosses itself.
65+
if grid[obstacle] == b'.' {
66+
if is_cycle(&shortcut, &mut seen, obstacle, position, direction) {
67+
result += 1;
68+
}
69+
// Reuse `seen` to minimize allocations.
70+
seen.clear();
71+
grid[obstacle] = b'^';
72+
}
73+
74+
position = obstacle;
75+
}
76+
77+
result
78+
}
79+
80+
fn is_cycle(
81+
shortcut: &Shortcut,
82+
seen: &mut FastSet<(Point, Point)>,
83+
obstacle: Point,
84+
mut position: Point,
85+
mut direction: Point,
86+
) -> bool {
87+
while shortcut.up.contains(position) {
88+
// Reaching the same position in the same direction is a cycle.
89+
if !seen.insert((position, direction)) {
90+
return true;
91+
}
92+
93+
// The tricky part is checking for the new time travelling instigated obstacle.
94+
position = match direction {
95+
UP => {
96+
let next = shortcut.up[position];
97+
if position.x == obstacle.x && position.y > obstacle.y && obstacle.y >= next.y {
98+
obstacle - UP
99+
} else {
100+
next
101+
}
102+
}
103+
DOWN => {
104+
let next = shortcut.down[position];
105+
if position.x == obstacle.x && position.y < obstacle.y && obstacle.y <= next.y {
106+
obstacle - DOWN
107+
} else {
108+
next
109+
}
110+
}
111+
LEFT => {
112+
let next = shortcut.left[position];
113+
if position.y == obstacle.y && position.x > obstacle.x && obstacle.x >= next.x {
114+
obstacle - LEFT
115+
} else {
116+
next
117+
}
118+
}
119+
RIGHT => {
120+
let next = shortcut.right[position];
121+
if position.y == obstacle.y && position.x < obstacle.x && obstacle.x <= next.x {
122+
obstacle - RIGHT
123+
} else {
124+
next
125+
}
126+
}
127+
_ => unreachable!(),
128+
};
129+
130+
direction = direction.clockwise();
131+
}
132+
133+
false
134+
}
135+
136+
struct Shortcut {
137+
up: Grid<Point>,
138+
down: Grid<Point>,
139+
left: Grid<Point>,
140+
right: Grid<Point>,
141+
}
142+
143+
impl Shortcut {
144+
fn from(grid: &Grid<u8>) -> Self {
145+
let mut up = copy(grid);
146+
let mut down = copy(grid);
147+
let mut left = copy(grid);
148+
let mut right = copy(grid);
149+
150+
for x in 0..grid.width {
151+
let mut last = Point::new(x, -1);
152+
153+
for y in 0..grid.height {
154+
let point = Point::new(x, y);
155+
if grid[point] == b'#' {
156+
last = Point::new(x, y + 1);
157+
}
158+
up[point] = last;
159+
}
160+
}
161+
162+
for x in 0..grid.width {
163+
let mut last = Point::new(x, grid.height);
164+
165+
for y in (0..grid.height).rev() {
166+
let point = Point::new(x, y);
167+
if grid[point] == b'#' {
168+
last = Point::new(x, y - 1);
169+
}
170+
down[point] = last;
171+
}
172+
}
173+
174+
for y in 0..grid.height {
175+
let mut last = Point::new(-1, y);
176+
177+
for x in 0..grid.width {
178+
let point = Point::new(x, y);
179+
if grid[point] == b'#' {
180+
last = Point::new(x + 1, y);
181+
}
182+
left[point] = last;
183+
}
184+
}
185+
186+
for y in 0..grid.height {
187+
let mut last = Point::new(grid.width, y);
188+
189+
for x in (0..grid.width).rev() {
190+
let point = Point::new(x, y);
191+
if grid[point] == b'#' {
192+
last = Point::new(x - 1, y);
193+
}
194+
right[point] = last;
195+
}
196+
}
197+
198+
Shortcut { up, down, left, right }
199+
}
200+
}
201+
202+
fn copy(grid: &Grid<u8>) -> Grid<Point> {
203+
Grid {
204+
width: grid.width,
205+
height: grid.height,
206+
bytes: vec![ORIGIN; (grid.width * grid.height) as usize],
207+
}
208+
}

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)