diff --git a/README.md b/README.md index 5eeabe6..19645af 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ pie | 12 | [The N-Body Problem](https://adventofcode.com/2019/day/12) | [Source](src/year2019/day12.rs) | 1024 | | 13 | [Care Package](https://adventofcode.com/2019/day/13) | [Source](src/year2019/day13.rs) | 3492 | | 14 | [Space Stoichiometry](https://adventofcode.com/2019/day/14) | [Source](src/year2019/day14.rs) | 17 | -| 15 | [Oxygen System](https://adventofcode.com/2019/day/15) | [Source](src/year2019/day15.rs) | 413 | +| 15 | [Oxygen System](https://adventofcode.com/2019/day/15) | [Source](src/year2019/day15.rs) | 442 | ## 2015 diff --git a/src/year2019/day15.rs b/src/year2019/day15.rs index 92e1347..5e91771 100644 --- a/src/year2019/day15.rs +++ b/src/year2019/day15.rs @@ -5,133 +5,99 @@ //! [This excellent blog](https://www.redblobgames.com/pathfinding/a-star/introduction.html) //! has more detail on the various path finding algorithms that come in handy during Advent of Code. //! -//! The tricky part is that our droid is stateful. If we move to a new location without fully -//! exploring the previous location, then we would somehow have to retrace our steps. -//! -//! This solution side-steps that issue by using the [`clone`] method to take a snapshot -//! of our droid at every location. We can then restart from any location using that snapshot. -//! Cloning is a relatively slow operation, due to the large `vec` of intcode instructions, so we -//! optimize by cloning as little as possible, using 3 tricks: -//! -//! * If the result of movement is `0` then the droid is still in the same location and can be -//! re-used without cloning. We store a clone of the droid in an option using the [`take`] and -//! [`replace`] methods to avoid the compiler complaining. -//! * We maximize the chances of getting a wall collision first by keeping track of the -//! direction that the droid came from. The maze consists mostly of long narrow corridors, so -//! for example if we are heading South, then trying East and West will most likely be walls. -//! We try South last and can skip North entirely as we came from that direction. -//! * Finally we don't need to any more clones when trying the last direction from a location -//! as there are no more possibilites to try. Otherwise we make a clone then restore the droid -//! to the previous location using a reverse direction command. -//! -//! For part two, we perform a *second* BFS, starting from the location of the oxygen system. This -//! uses the data gathered from part one, so is much faster as it doesn't need to run the -//! intcode program. -//! -//! [`clone`]: std::clone::Clone -//! [`take`]: Option::take -//! [`replace`]: Option::replace +//! The tricky part is determining the shape of the maze. If we assume the maze consists only of +//! corridors of width one and has no loops or rooms, then we can use the simple +//! [wall follower](https://en.wikipedia.org/wiki/Maze-solving_algorithm#Wall_follower) +//! algorithm to eventually trace our way through the entire maze back to the starting point. use super::day09::intcode::*; use crate::util::hash::*; use crate::util::parse::*; use crate::util::point::*; use std::collections::VecDeque; -/// Maximize chances of colliding with a wall when searching directions. The maze consists -/// of long narrow corridors, so checking perpendicular to our direction will most likely -/// hit a wall. -const DIRECTIONS: [&[(i64, Point)]; 5] = [ - &[(1, UP), (2, DOWN), (3, LEFT), (4, RIGHT)], - &[(3, LEFT), (4, RIGHT), (1, UP)], - &[(3, LEFT), (4, RIGHT), (2, DOWN)], - &[(1, UP), (2, DOWN), (3, LEFT)], - &[(1, UP), (2, DOWN), (4, RIGHT)], -]; - -/// Reverse direction lookup used to restore droid to previous location. -const REVERSE: [i64; 5] = [0, 2, 1, 4, 3]; - -type Input = (i32, i32); +type Input = (FastSet, Point); +/// Build the shape of the maze using the right-hand version of the wall following algorithm. pub fn parse(input: &str) -> Input { let code: Vec<_> = input.iter_signed().collect(); - let computer = Computer::new(&code); - - // Breadth first search over all possible points in the maze. As we need the complete maze for - // part two we intentionally don't exit early when the oxygen system is found. - let mut todo = VecDeque::from([(0, 0, ORIGIN, computer)]); - let mut visited = FastSet::build([ORIGIN]); - let mut oxygen_system = (0, ORIGIN); - - while let Some((cost, from, point, computer)) = todo.pop_front() { - let limit = DIRECTIONS[from as usize].len() - 1; - let iter = DIRECTIONS[from as usize].iter().enumerate(); - - let mut storage = Some(computer); + let mut computer = Computer::new(&code); + let mut first = true; + let mut direction = UP; + let mut position = ORIGIN; + let mut oxygen_system = ORIGIN; + let mut visited = FastSet::new(); + + loop { + direction = if first { direction.clockwise() } else { direction.counter_clockwise() }; + + match direction { + UP => computer.input(&[1]), + DOWN => computer.input(&[2]), + LEFT => computer.input(&[3]), + RIGHT => computer.input(&[4]), + _ => unreachable!(), + } - for (index, &(command, movement)) in iter { - let next_cost = cost + 1; - let next_point = point + movement; + match computer.run() { + State::Output(0) => first = false, + State::Output(result) => { + first = true; + position += direction; + visited.insert(position); - if visited.contains(&next_point) { - continue; + if result == 2 { + oxygen_system = position; + } + if position == ORIGIN { + break; + } } + _ => unreachable!(), + } + } - let mut next_computer = storage.take().unwrap(); - next_computer.input(&[command]); - let State::Output(result) = next_computer.run() else { - unreachable!(); - }; + (visited, oxygen_system) +} - if result == 0 { - // The droid is still at the same location, so put it back into the option. - storage.replace(next_computer); - } else { - if result == 2 { - oxygen_system = (next_cost, next_point); - } +/// BFS from the starting point until we find the oxygen system. +pub fn part1(input: &Input) -> i32 { + let (mut maze, oxygen_system) = input.clone(); + let mut todo = VecDeque::from([(ORIGIN, 0)]); - // We moved so restore the snapshot of previous location as long as there - // are more directions to try. - if index < limit { - let mut restore = next_computer.clone(); - restore.input(&[REVERSE[command as usize]]); - restore.run(); - storage.replace(restore); - } + while let Some((point, cost)) = todo.pop_front() { + maze.remove(&point); + if point == oxygen_system { + return cost; + } - todo.push_back((next_cost, command, next_point, next_computer)); - visited.insert(next_point); + for movement in ORTHOGONAL { + let next_point = point + movement; + if maze.contains(&next_point) { + todo.push_back((next_point, cost + 1)); } } } - // Start a second search from the location of the oxygen system. To speed things up we re-use - // the points from the first search to avoid needing to run the intcode program. - let mut todo = VecDeque::from([(0, oxygen_system.1)]); + unreachable!() +} + +/// BFS from the oxygen system to all points in the maze. +pub fn part2(input: &Input) -> i32 { + let (mut maze, oxygen_system) = input.clone(); + let mut todo = VecDeque::from([(oxygen_system, 0)]); let mut minutes = 0; - while let Some((cost, point)) = todo.pop_front() { - visited.remove(&point); + while let Some((point, cost)) = todo.pop_front() { + maze.remove(&point); minutes = minutes.max(cost); for movement in ORTHOGONAL { - let next_cost = cost + 1; let next_point = point + movement; - - if visited.contains(&next_point) { - todo.push_back((next_cost, next_point)); + if maze.contains(&next_point) { + todo.push_back((next_point, cost + 1)); } } } - (oxygen_system.0, minutes) -} - -pub fn part1(input: &Input) -> i32 { - input.0 -} - -pub fn part2(input: &Input) -> i32 { - input.1 + minutes }