Skip to content

Commit 7bd0703

Browse files
committed
Year 2024 Day 15
1 parent 72ec737 commit 7bd0703

File tree

7 files changed

+239
-4
lines changed

7 files changed

+239
-4
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Performance is reasonable even on older hardware, for example a 2011 MacBook Pro
8686
| 12 | [Garden Groups](https://adventofcode.com/2024/day/12) | [Source](src/year2024/day12.rs) | 289 |
8787
| 13 | [Claw Contraption](https://adventofcode.com/2024/day/13) | [Source](src/year2024/day13.rs) | 14 |
8888
| 14 | [Restroom Redoubt](https://adventofcode.com/2024/day/14) | [Source](src/year2024/day14.rs) | 74 |
89+
| 15 | [Warehouse Woes](https://adventofcode.com/2024/day/15) | [Source](src/year2024/day15.rs) | 332 |
8990

9091
## 2023
9192

benches/benchmark.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,5 @@ benchmark!(year2023
8888

8989
benchmark!(year2024
9090
day01, day02, day03, day04, day05, day06, day07, day08, day09, day10, day11, day12, day13,
91-
day14
91+
day14, day15
9292
);

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,5 @@ library!(year2023 "Restore global snow production."
6868

6969
library!(year2024 "Locate the Chief Historian in time for the big Christmas sleigh launch."
7070
day01, day02, day03, day04, day05, day06, day07, day08, day09, day10, day11, day12, day13,
71-
day14
71+
day14, day15
7272
);

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,5 +140,5 @@ run!(year2023
140140

141141
run!(year2024
142142
day01, day02, day03, day04, day05, day06, day07, day08, day09, day10, day11, day12, day13,
143-
day14
143+
day14, day15
144144
);

src/year2024/day15.rs

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
//! # Warehouse Woes
2+
//!
3+
//! Festive version of [Sokoban](https://en.wikipedia.org/wiki/Sokoban).
4+
//!
5+
//! Part one loops in a straight line looking for the next space `.` or wall `#`. No bounds checks
6+
//! are needed as the maze is enclosed. If a space is found then all items are pushed one block
7+
//! in that direction.
8+
//!
9+
//! Part two re-uses the part one logic for horizontal moves. Vertical moves use a
10+
//! [breadth first search](https://en.wikipedia.org/wiki/Breadth-first_search) to identify the
11+
//! cascading boxes that need to be moved. Marking boxes as `seen` during the search prevents both
12+
//! unintended exponential growth or pushing the bottom most row twice in this example:
13+
//!
14+
//! ```none
15+
//! @
16+
//! []
17+
//! [][]
18+
//! []
19+
//! ```
20+
//!
21+
//! If any next space is a wall then we cancel the entire move and return right away. Otherwise
22+
//! all boxes are moved in the *reverse* order that they were found by the search.
23+
use crate::util::grid::*;
24+
use crate::util::point::*;
25+
use std::mem::swap;
26+
27+
type Input<'a> = (Grid<u8>, &'a str);
28+
29+
pub fn parse(input: &str) -> Input<'_> {
30+
let (prefix, suffix) = input.split_once("\n\n").unwrap();
31+
let grid = Grid::parse(prefix);
32+
(grid, suffix)
33+
}
34+
35+
pub fn part1(input: &Input<'_>) -> i32 {
36+
let (grid, moves) = input;
37+
38+
let mut grid = grid.clone();
39+
let mut position = grid.find(b'@').unwrap();
40+
41+
// Treat moves as a single string ignoring any newline characters.
42+
for b in moves.bytes() {
43+
match b {
44+
b'<' => narrow(&mut grid, &mut position, LEFT),
45+
b'>' => narrow(&mut grid, &mut position, RIGHT),
46+
b'^' => narrow(&mut grid, &mut position, UP),
47+
b'v' => narrow(&mut grid, &mut position, DOWN),
48+
_ => (),
49+
}
50+
}
51+
52+
gps(&grid, b'O')
53+
}
54+
55+
pub fn part2(input: &Input<'_>) -> i32 {
56+
let (grid, moves) = input;
57+
58+
let mut grid = stretch(grid);
59+
let mut position = grid.find(b'@').unwrap();
60+
61+
// Reuse to minimize allocations.
62+
let mut todo = Vec::new();
63+
let mut seen = grid.same_size_with(usize::MAX);
64+
65+
// Use index as a unique id for each move.
66+
for (id, b) in moves.bytes().enumerate() {
67+
match b {
68+
b'<' => narrow(&mut grid, &mut position, LEFT),
69+
b'>' => narrow(&mut grid, &mut position, RIGHT),
70+
b'^' => wide(&mut grid, &mut position, UP, &mut todo, &mut seen, id),
71+
b'v' => wide(&mut grid, &mut position, DOWN, &mut todo, &mut seen, id),
72+
_ => (),
73+
}
74+
}
75+
76+
gps(&grid, b'[')
77+
}
78+
79+
fn narrow(grid: &mut Grid<u8>, start: &mut Point, direction: Point) {
80+
let mut position = *start + direction;
81+
let mut size = 2;
82+
83+
// Search for the next wall or open space.
84+
while grid[position] != b'.' && grid[position] != b'#' {
85+
position += direction;
86+
size += 1;
87+
}
88+
89+
// Move items one space in direction.
90+
if grid[position] == b'.' {
91+
let mut previous = b'.';
92+
let mut position = *start;
93+
94+
for _ in 0..size {
95+
swap(&mut previous, &mut grid[position]);
96+
position += direction;
97+
}
98+
99+
// Move robot
100+
*start += direction;
101+
}
102+
}
103+
104+
fn wide(
105+
grid: &mut Grid<u8>,
106+
start: &mut Point,
107+
direction: Point,
108+
todo: &mut Vec<Point>,
109+
seen: &mut Grid<usize>,
110+
id: usize,
111+
) {
112+
// Short circuit if path in front of robot is empty.
113+
let position = *start;
114+
let next = position + direction;
115+
116+
if grid[next] == b'.' {
117+
grid[position] = b'.';
118+
grid[next] = b'@';
119+
*start += direction;
120+
return;
121+
}
122+
123+
// Clear any items from previous push.
124+
todo.clear();
125+
todo.push(*start);
126+
let mut index = 0;
127+
128+
while index < todo.len() {
129+
let next = todo[index] + direction;
130+
index += 1;
131+
132+
let other = match grid[next] {
133+
b'#' => return, // Return early if there's a wall in the way.
134+
b'[' => RIGHT,
135+
b']' => LEFT,
136+
_ => continue, // Open space doesn't add any more items to move.
137+
};
138+
139+
// Enqueue the first half of box directly above us.
140+
let first = next;
141+
if seen[first] != id {
142+
seen[first] = id;
143+
todo.push(first);
144+
}
145+
146+
// Enqueue the other half of the box directly above us.
147+
let second = next + other;
148+
if seen[second] != id {
149+
seen[second] = id;
150+
todo.push(second);
151+
}
152+
}
153+
154+
// Move boxes in reverse order.
155+
for &point in todo.iter().rev() {
156+
grid[point + direction] = grid[point];
157+
grid[point] = b'.';
158+
}
159+
160+
// Move robot
161+
*start += direction;
162+
}
163+
164+
fn stretch(grid: &Grid<u8>) -> Grid<u8> {
165+
let mut next = Grid::new(grid.width * 2, grid.height, b'.');
166+
167+
for y in 0..grid.height {
168+
for x in 0..grid.width {
169+
// Grid is already filled with '.', so only need to handle other kinds.
170+
let (left, right) = match grid[Point::new(x, y)] {
171+
b'#' => (b'#', b'#'),
172+
b'O' => (b'[', b']'),
173+
b'@' => (b'@', b'.'),
174+
_ => continue,
175+
};
176+
177+
next[Point::new(2 * x, y)] = left;
178+
next[Point::new(2 * x + 1, y)] = right;
179+
}
180+
}
181+
182+
next
183+
}
184+
185+
fn gps(grid: &Grid<u8>, needle: u8) -> i32 {
186+
let mut result = 0;
187+
188+
for y in 0..grid.height {
189+
for x in 0..grid.width {
190+
let point = Point::new(x, y);
191+
if grid[point] == needle {
192+
result += 100 * point.y + point.x;
193+
}
194+
}
195+
}
196+
197+
result
198+
}

tests/test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,5 +81,5 @@ test!(year2023
8181

8282
test!(year2024
8383
day01, day02, day03, day04, day05, day06, day07, day08, day09, day10, day11, day12, day13,
84-
day14
84+
day14, day15
8585
);

tests/year2024/day15.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use aoc::year2024::day15::*;
2+
3+
const EXAMPLE: &str = "\
4+
##########
5+
#..O..O.O#
6+
#......O.#
7+
#.OO..O.O#
8+
9+
#O#..O...#
10+
#O..O..O.#
11+
#.OO.O.OO#
12+
#....O...#
13+
##########
14+
15+
<vv>^<v^>v>^vv^v>v<>v^v<v<^vv<<<^><<><>>v<vvv<>^v^>^<<<><<v<<<v^vv^v>^
16+
vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<<v<^v>^<^^>>>^<v<v
17+
><>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^<v>v^^<^^vv<
18+
<<v<^>>^^^^>>>v^<>vvv^><v<<<>^^^vv^<vvv>^>v<^^^^v<>^>vvvv><>>v^<<^^^^^
19+
^><^><>>><>^^<<^^v>>><^<v>^<vv>>v>>>^v><>^v><<<<v>>v<v<v>vvv>^<><<>^><
20+
^>><>^v<><^vvv<^^<><v<<<<<><^v<<<><<<^^<v<^^^><^>>^<v^><<<^>>^v<v^v<v^
21+
>^>>^v>vv>^<<^v<>><<><<v<<v><>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^
22+
<><^^>^^^<><vvvvv^v<v<<>^v<v>v<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<>
23+
^^>vv<^v^v<vv>^<><v<^v>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<><<v>
24+
v^^>>><<^^<>>^v^<v^vv<>v^<<>^<^v^v><^<<<><<^<v><v<>vv>>v><v^<vv<>v^<<^";
25+
26+
#[test]
27+
fn part1_test() {
28+
let input = parse(EXAMPLE);
29+
assert_eq!(part1(&input), 10092);
30+
}
31+
32+
#[test]
33+
fn part2_test() {
34+
let input = parse(EXAMPLE);
35+
assert_eq!(part2(&input), 9021);
36+
}

0 commit comments

Comments
 (0)