diff --git a/README.md b/README.md index 4868e8f..0666097 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Thank you to [Eric Wastl](http://was.tl) for running this incredible yearly even - [Day 17: Chronospatial Computer](aoc_2024/src/day_17.rs) - [Day 18: RAM Run](aoc_2024/src/day_18.rs) - [Day 19: Linen Layout](aoc_2024/src/day_19.rs) +- [Day 20: Race Condition](aoc_2024/src/day_20.rs) ## [2023](https://adventofcode.com/2023) [![aoc_2023](https://github.com/connorslade/advent-of-code/actions/workflows/aoc_2023.yml/badge.svg)](https://github.com/connorslade/advent-of-code/actions/workflows/aoc_2023.yml) diff --git a/aoc_2024/src/day_20.rs b/aoc_2024/src/day_20.rs new file mode 100644 index 0000000..348f686 --- /dev/null +++ b/aoc_2024/src/day_20.rs @@ -0,0 +1,121 @@ +use std::{collections::VecDeque, convert::identity, u32}; + +use aoc_lib::{direction::cardinal::Direction, matrix::Grid}; +use common::{solution, Answer}; +use itertools::Itertools; +use nd_vec::{vector, Vec2}; + +solution!("Race Condition", 20); + +fn part_a(input: &str) -> Answer { + Problem::parse(input).solve(2).into() +} + +fn part_b(input: &str) -> Answer { + Problem::parse(input).solve(20).into() +} + +struct Problem { + board: Grid, + start: Vec2, + end: Vec2, +} + +impl Problem { + fn parse(input: &str) -> Self { + let board = Grid::parse(input, identity); + + let start = board.find('S').unwrap(); + let end = board.find('E').unwrap(); + + Self { board, start, end } + } + + fn solve(&self, max_skip: i32) -> u32 { + let (sc, ec) = (self.cost_map(self.start), self.cost_map(self.end)); + let base_cost = sc[self.end]; + + let mut out = 0; + + for (pos, tile) in self.board.iter() { + if *tile == '#' || sc[pos] == u32::MAX { + continue; + } + + for (x, y) in (-max_skip..=max_skip).cartesian_product(-max_skip..=max_skip) { + let offset = vector!(x, y); + let dist = offset.manhattan_distance(&Vec2::zero()); + if dist > max_skip { + continue; + } + + let end = pos.try_cast::().unwrap() + offset; + if !self.board.contains(end) || self.board[end] == '#' || ec[end] == u32::MAX { + continue; + } + + let cost = sc[pos] + ec[end] + dist as u32; + out += (cost + 100 <= base_cost) as u32; + } + } + + out + } + + fn cost_map(&self, start: Vec2) -> Grid { + let mut costs = Grid::new(self.board.size, u32::MAX); + let mut queue = VecDeque::new(); + queue.push_back((start, 0)); + + while let Some((pos, dist)) = queue.pop_front() { + if costs[pos] != u32::MAX { + continue; + } + + costs[pos] = dist; + for dir in Direction::ALL { + let next = dir.wrapping_advance(pos); + if let Some(tile) = self.board.get(next) { + if matches!(tile, '.' | 'E') { + queue.push_back((next, dist + 1)); + } + } + } + } + + costs + } +} + +#[cfg(test)] +mod test { + use indoc::indoc; + + const CASE: &str = indoc! {" + ############### + #...#...#.....# + #.#.#.#.#.###.# + #S#...#.#.#...# + #######.#.#.### + #######.#.#...# + #######.#.###.# + ###..E#...#...# + ###.#######.### + #...###...#...# + #.#####.#.###.# + #.#...#.#.#...# + #.#.#.#.#.#.### + #...#...#...### + ############### + "}; + + #[test] + fn part_a() { + assert_eq!(super::part_a(CASE), 44.into()); + } + + #[test] + fn part_b() { + assert_eq!(super::part_b(CASE), ().into()); + } +} diff --git a/aoc_2024/src/lib.rs b/aoc_2024/src/lib.rs index 4c6db6c..12e0848 100644 --- a/aoc_2024/src/lib.rs +++ b/aoc_2024/src/lib.rs @@ -19,6 +19,7 @@ mod day_16; mod day_17; mod day_18; mod day_19; +mod day_20; // [import_marker] pub const SOLUTIONS: &[Solution] = &[ @@ -41,5 +42,6 @@ pub const SOLUTIONS: &[Solution] = &[ day_17::SOLUTION, day_18::SOLUTION, day_19::SOLUTION, + day_20::SOLUTION, // [list_marker] ];