Skip to main content

aoc/year2024/
day15.rs

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 reuses 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. Boxes are added strictly left to right to make checking
12//! for previously added boxes easier. To prevent adding a box twice we check that the
13//! item at `index - 2` is different. For example:
14//!
15//! ```none
16//!     @          Indices:
17//!     []         23
18//!    [][]       4567
19//!     []         89
20//! ```
21//!
22//! When processing 6 we try to add 8, however 8 and 9 have already been added when processing 4
23//! so we skip.
24//!
25//! If any next space is a wall then we cancel the entire move and return right away. Otherwise
26//! all boxes are moved in the *reverse* order that they were found by the search.
27use crate::util::grid::*;
28use crate::util::point::*;
29use std::mem::swap;
30
31type Input<'a> = (Grid<u8>, &'a str);
32
33pub fn parse(input: &str) -> Input<'_> {
34    let (prefix, suffix) = input.split_once("\n\n").unwrap();
35    let grid = Grid::parse(prefix);
36    (grid, suffix)
37}
38
39pub fn part1(input: &Input<'_>) -> i32 {
40    let (grid, moves) = input;
41
42    // We don't need to move the robot symbol so mark as empty space once located.
43    let mut grid = grid.clone();
44    let mut position = grid.find(b'@').unwrap();
45    grid[position] = b'.';
46
47    // Treat moves as a single string ignoring any newline characters.
48    for b in moves.bytes() {
49        match b {
50            b'<' => narrow(&mut grid, &mut position, LEFT),
51            b'>' => narrow(&mut grid, &mut position, RIGHT),
52            b'^' => narrow(&mut grid, &mut position, UP),
53            b'v' => narrow(&mut grid, &mut position, DOWN),
54            _ => (),
55        }
56    }
57
58    gps(&grid, b'O')
59}
60
61pub fn part2(input: &Input<'_>) -> i32 {
62    let (grid, moves) = input;
63
64    let mut grid = stretch(grid);
65    let mut position = grid.find(b'@').unwrap();
66    grid[position] = b'.';
67
68    // Reuse to minimize allocations.
69    let mut todo = Vec::with_capacity(50);
70
71    // Horizontal moves reuse the part one logic, vertical moves need to cascade.
72    for b in moves.bytes() {
73        match b {
74            b'<' | b'>' => narrow(&mut grid, &mut position, Point::from(b)),
75            b'^' | b'v' => wide(&mut grid, &mut position, Point::from(b), &mut todo),
76            _ => (),
77        }
78    }
79
80    gps(&grid, b'[')
81}
82
83fn narrow(grid: &mut Grid<u8>, start: &mut Point, direction: Point) {
84    let mut position = *start + direction;
85    let mut size = 1;
86
87    // Search for the next wall or open space.
88    while grid[position] != b'.' && grid[position] != b'#' {
89        position += direction;
90        size += 1;
91    }
92
93    // Move items one space in direction.
94    if grid[position] == b'.' {
95        let mut previous = b'.';
96        let mut position = *start + direction;
97
98        for _ in 0..size {
99            swap(&mut previous, &mut grid[position]);
100            position += direction;
101        }
102
103        // Move robot.
104        *start += direction;
105    }
106}
107
108fn wide(grid: &mut Grid<u8>, start: &mut Point, direction: Point, todo: &mut Vec<Point>) {
109    // Short circuit if path in front of robot is empty.
110    if grid[*start + direction] == b'.' {
111        *start += direction;
112        return;
113    }
114
115    // Clear any items from previous push.
116    todo.clear();
117    // Add dummy item to prevent index out of bounds when checking for previously added boxes.
118    todo.push(ORIGIN);
119    todo.push(*start);
120    let mut index = 1;
121
122    while index < todo.len() {
123        let next = todo[index] + direction;
124        index += 1;
125
126        // Add boxes strictly left to right.
127        let (first, second) = match grid[next] {
128            b'[' => (next, next + RIGHT),
129            b']' => (next + LEFT, next),
130            b'#' => return, // Return early if there's a wall in the way.
131            _ => continue,  // Open space doesn't add any more items to move.
132        };
133
134        // Check if this box has already been added by the previous box in this row.
135        if first != todo[todo.len() - 2] {
136            todo.push(first);
137            todo.push(second);
138        }
139    }
140
141    // Move boxes in reverse order, skipping the dummy item and robot.
142    for &point in todo[2..].iter().rev() {
143        grid[point + direction] = grid[point];
144        grid[point] = b'.';
145    }
146
147    // Move robot.
148    *start += direction;
149}
150
151fn stretch(grid: &Grid<u8>) -> Grid<u8> {
152    let mut next = Grid::new(grid.width * 2, grid.height, b'.');
153
154    for y in 0..grid.height {
155        for x in 0..grid.width {
156            // Grid is already filled with '.', so only need to handle other kinds.
157            let (left, right) = match grid[Point::new(x, y)] {
158                b'#' => (b'#', b'#'),
159                b'O' => (b'[', b']'),
160                b'@' => (b'@', b'.'),
161                _ => continue,
162            };
163
164            next[Point::new(2 * x, y)] = left;
165            next[Point::new(2 * x + 1, y)] = right;
166        }
167    }
168
169    next
170}
171
172fn gps(grid: &Grid<u8>, needle: u8) -> i32 {
173    let mut result = 0;
174
175    for y in 0..grid.height {
176        for x in 0..grid.width {
177            let point = Point::new(x, y);
178            if grid[point] == needle {
179                result += 100 * point.y + point.x;
180            }
181        }
182    }
183
184    result
185}