Day 15: Warehouse Woes
Megathread guidelines
- Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
- You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
Rust
Part 2 was a bit tricky. Moving into a box horizontally works mostly the same as for part 1, for the vertical case I used two recursive functions. The first recurses from the left and right side for each box just to find out if the entire tree can be moved. The second function actually does the moving in a similar recursive structure, but now with the knowledge that all subtrees can actually be moved.
Lots of moving parts, but at least it could very nicely be debugged by printing out the map from the two minimal examples after each round.
Solution
use euclid::{default::*, vec2};
// Common type for both parts. In part 1 all boxes are BoxL.
#[derive(Clone, Copy)]
enum Spot {
Empty,
BoxL,
BoxR,
Wall,
}
impl From<u8> for Spot {
fn from(value: u8) -> Self {
match value {
b'.' | b'@' => Spot::Empty,
b'O' => Spot::BoxL,
b'#' => Spot::Wall,
other => panic!("Invalid spot: {other}"),
}
}
}
fn parse(input: &str) -> (Vec<Vec<Spot>>, Point2D<i32>, Vec<Vector2D<i32>>) {
let (field_s, moves_s) = input.split_once("\n\n").unwrap();
let mut field = Vec::new();
let mut robot = None;
for (y, l) in field_s.lines().enumerate() {
let mut row = Vec::new();
for (x, b) in l.bytes().enumerate() {
row.push(Spot::from(b));
if b == b'@' {
robot = Some(Point2D::new(x, y).to_i32())
}
}
field.push(row);
}
let moves = moves_s
.bytes()
.filter(|b| *b != b'\n')
.map(|b| match b {
b'^' => vec2(0, -1),
b'>' => vec2(1, 0),
b'v' => vec2(0, 1),
b'<' => vec2(-1, 0),
other => panic!("Invalid move: {other}"),
})
.collect();
(field, robot.unwrap(), moves)
}
fn gps(field: &[Vec<Spot>]) -> u32 {
let mut sum = 0;
for (y, row) in field.iter().enumerate() {
for (x, s) in row.iter().enumerate() {
if let Spot::BoxL = s {
sum += x + 100 * y;
}
}
}
sum as u32
}
fn part1(input: String) {
let (mut field, mut robot, moves) = parse(&input);
for m in moves {
let next = robot + m;
match field[next.y as usize][next.x as usize] {
Spot::Empty => robot = next, // Move into space
Spot::BoxL => {
let mut search = next + m;
let can_move = loop {
match field[search.y as usize][search.x as usize] {
Spot::BoxL => {}
Spot::Wall => break false,
Spot::Empty => break true,
Spot::BoxR => unreachable!(),
}
search += m;
};
if can_move {
robot = next;
field[next.y as usize][next.x as usize] = Spot::Empty;
field[search.y as usize][search.x as usize] = Spot::BoxL;
}
}
Spot::Wall => {} // Cannot move
Spot::BoxR => unreachable!(),
}
}
println!("{}", gps(&field));
}
// Transform part 1 field to wider part 2 field
fn widen(field: &[Vec<Spot>]) -> Vec<Vec<Spot>> {
field
.iter()
.map(|row| {
row.iter()
.flat_map(|s| match s {
Spot::Empty => [Spot::Empty; 2],
Spot::Wall => [Spot::Wall; 2],
Spot::BoxL => [Spot::BoxL, Spot::BoxR],
Spot::BoxR => unreachable!(),
})
.collect()
})
.collect()
}
// Recursively find out whether or not the robot can move in direction `dir` from `start`.
fn can_move_rec(field: &[Vec<Spot>], start: Point2D<i32>, dir: Vector2D<i32>) -> bool {
let next = start + dir;
match field[next.y as usize][next.x as usize] {
Spot::Empty => true,
Spot::BoxL => can_move_rec(field, next, dir) && can_move_rec(field, next + vec2(1, 0), dir),
Spot::BoxR => can_move_rec(field, next - vec2(1, 0), dir) && can_move_rec(field, next, dir),
Spot::Wall => false,
}
}
// Recursively execute a move for vertical directions into boxes.
fn do_move(field: &mut [Vec<Spot>], start: Point2D<i32>, dir: Vector2D<i32>) {
let next = start + dir;
match field[next.y as usize][next.x as usize] {
Spot::Empty | Spot::Wall => {}
Spot::BoxL => {
do_move(field, next, dir);
do_move(field, next + vec2(1, 0), dir);
let move_to = next + dir;
field[next.y as usize][next.x as usize] = Spot::Empty;
field[next.y as usize][next.x as usize + 1] = Spot::Empty;
field[move_to.y as usize][move_to.x as usize] = Spot::BoxL;
field[move_to.y as usize][move_to.x as usize + 1] = Spot::BoxR;
}
Spot::BoxR => {
do_move(field, next - vec2(1, 0), dir);
do_move(field, next, dir);
let move_to = next + dir;
field[next.y as usize][next.x as usize - 1] = Spot::Empty;
field[next.y as usize][next.x as usize] = Spot::Empty;
field[move_to.y as usize][move_to.x as usize - 1] = Spot::BoxL;
field[move_to.y as usize][move_to.x as usize] = Spot::BoxR;
}
}
}
fn part2(input: String) {
let (field1, robot1, moves) = parse(&input);
let mut field = widen(&field1);
let mut robot = Point2D::new(robot1.x * 2, robot1.y);
for m in moves {
let next = robot + m;
match field[next.y as usize][next.x as usize] {
Spot::Empty => robot = next, // Move into space
Spot::BoxL | Spot::BoxR if m.y == 0 => {
let mut search = next + m;
let can_move = loop {
match field[search.y as usize][search.x as usize] {
Spot::BoxL | Spot::BoxR => {}
Spot::Wall => break false,
Spot::Empty => break true,
}
search += m;
};
if can_move {
robot = next;
// Shift boxes by array remove/insert
field[next.y as usize].remove(search.x as usize);
field[next.y as usize].insert(next.x as usize, Spot::Empty);
}
}
Spot::BoxL | Spot::BoxR => {
if can_move_rec(&field, robot, m) {
do_move(&mut field, robot, m);
robot = next;
}
}
Spot::Wall => {} // Cannot move
}
}
println!("{}", gps(&field));
}
util::aoc_main!();
Also on github