233 lines
6.7 KiB
Rust
233 lines
6.7 KiB
Rust
use std::{
|
|
collections::{HashMap, HashSet},
|
|
error::Error,
|
|
};
|
|
|
|
use itertools::Itertools;
|
|
|
|
pub fn process_part1(input: &str) -> u32 {
|
|
simulate_all(input, 100, 2).values().copied().sum()
|
|
}
|
|
|
|
pub fn process_part2(input: &str) -> u32 {
|
|
simulate_all(input, 100, 20).values().copied().sum()
|
|
}
|
|
|
|
fn simulate_all(input: &str, time_to_save: usize, max_cheat_time: usize) -> HashMap<u32, u32> {
|
|
let mut start = (0, 0);
|
|
let grid = input
|
|
.lines()
|
|
.enumerate()
|
|
.map(|(yidx, row)| {
|
|
row.chars()
|
|
.enumerate()
|
|
.map(|(xidx, chara)| {
|
|
let tile = GridTile::try_from(chara).unwrap();
|
|
if tile == GridTile::Start {
|
|
start = (xidx, yidx);
|
|
}
|
|
tile
|
|
})
|
|
.collect_vec()
|
|
})
|
|
.collect_vec();
|
|
let no_cheat = simulate(&grid, start);
|
|
let mut saved = HashMap::new();
|
|
for (tile_idx, tile) in no_cheat[..no_cheat.len() - time_to_save].iter().enumerate() {
|
|
for (cheat_idx, cheat) in no_cheat[tile_idx..].iter().enumerate() {
|
|
let manhattan = tile.0.abs_diff(cheat.0) + tile.1.abs_diff(cheat.1);
|
|
if manhattan <= max_cheat_time {
|
|
let time_saved = cheat_idx - manhattan;
|
|
if time_saved >= time_to_save {
|
|
saved
|
|
.entry(time_saved as u32)
|
|
.and_modify(|count| *count += 1)
|
|
.or_insert(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
saved
|
|
}
|
|
|
|
fn simulate(grid: &[Vec<GridTile>], start: (usize, usize)) -> Vec<(usize, usize)> {
|
|
let mut visited = HashSet::new();
|
|
visited.insert(start);
|
|
let mut next_paths = vec![MazeRunner {
|
|
coords: start,
|
|
visited: visited.clone(),
|
|
..Default::default()
|
|
}];
|
|
let mut visited = Vec::new();
|
|
visited.push(start);
|
|
let mut arrived: Vec<MazeRunner> = Vec::new();
|
|
while !next_paths.is_empty() {
|
|
next_paths = next_paths
|
|
.iter()
|
|
.map(|maze_runner| {
|
|
let mut paths = Vec::new();
|
|
if let Some(path) = maze_runner.get_next(grid, Direction::Up) {
|
|
paths.push(path);
|
|
}
|
|
if let Some(path) = maze_runner.get_next(grid, Direction::Down) {
|
|
paths.push(path);
|
|
}
|
|
if let Some(path) = maze_runner.get_next(grid, Direction::Left) {
|
|
paths.push(path);
|
|
}
|
|
if let Some(path) = maze_runner.get_next(grid, Direction::Right) {
|
|
paths.push(path);
|
|
}
|
|
paths
|
|
})
|
|
.collect_vec()
|
|
.concat();
|
|
for (idx, maze_runner) in next_paths.clone().iter().enumerate().rev() {
|
|
if maze_runner.visited.contains(&maze_runner.coords)
|
|
|| visited.contains(&maze_runner.coords)
|
|
{
|
|
next_paths.remove(idx);
|
|
continue;
|
|
}
|
|
visited.push(maze_runner.coords);
|
|
next_paths[idx].visited.insert(maze_runner.coords);
|
|
if maze_runner.state == State::Arrived {
|
|
let arrived_reindeer = next_paths.remove(idx);
|
|
arrived.push(arrived_reindeer);
|
|
}
|
|
}
|
|
}
|
|
visited
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
enum Direction {
|
|
Up,
|
|
Down,
|
|
Left,
|
|
Right,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
|
enum State {
|
|
#[default]
|
|
Going,
|
|
Arrived,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
|
struct MazeRunner {
|
|
coords: (usize, usize),
|
|
visited: HashSet<(usize, usize)>,
|
|
state: State,
|
|
}
|
|
|
|
impl MazeRunner {
|
|
fn get_next(&self, grid: &[Vec<GridTile>], direction: Direction) -> Option<MazeRunner> {
|
|
let tile = match direction {
|
|
Direction::Up => grid[self.coords.1 - 1][self.coords.0],
|
|
Direction::Down => grid[self.coords.1 + 1][self.coords.0],
|
|
Direction::Right => grid[self.coords.1][self.coords.0 + 1],
|
|
Direction::Left => grid[self.coords.1][self.coords.0 - 1],
|
|
};
|
|
let coords = match direction {
|
|
Direction::Up => (self.coords.0, self.coords.1 - 1),
|
|
Direction::Down => (self.coords.0, self.coords.1 + 1),
|
|
Direction::Left => (self.coords.0 - 1, self.coords.1),
|
|
Direction::Right => (self.coords.0 + 1, self.coords.1),
|
|
};
|
|
if tile == GridTile::Wall {
|
|
None
|
|
} else if tile == GridTile::End {
|
|
Some(MazeRunner {
|
|
state: State::Arrived,
|
|
coords,
|
|
..self.clone()
|
|
})
|
|
} else {
|
|
Some(MazeRunner {
|
|
coords,
|
|
..self.clone()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
|
|
enum GridTile {
|
|
Wall,
|
|
#[default]
|
|
Path,
|
|
Start,
|
|
End,
|
|
}
|
|
|
|
impl TryFrom<char> for GridTile {
|
|
type Error = Box<dyn Error>;
|
|
|
|
fn try_from(value: char) -> std::result::Result<GridTile, Box<dyn Error>> {
|
|
match value {
|
|
'#' => Ok(Self::Wall),
|
|
'.' => Ok(Self::Path),
|
|
'S' => Ok(Self::Start),
|
|
'E' => Ok(Self::End),
|
|
_ => Err(Box::from(format!("{value} is not a valid tile"))),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::time::Instant;
|
|
|
|
use utils::time::get_elapsed_string;
|
|
|
|
use super::*;
|
|
|
|
const INPUT: &str = "###############
|
|
#...#...#.....#
|
|
#.#.#.#.#.###.#
|
|
#S#...#.#.#...#
|
|
#######.#.#.###
|
|
#######.#.#...#
|
|
#######.#.###.#
|
|
###..E#...#...#
|
|
###.#######.###
|
|
#...###...#...#
|
|
#.#####.#.###.#
|
|
#.#...#.#.#...#
|
|
#.#.#.#.#.#.###
|
|
#...#...#...###
|
|
###############";
|
|
|
|
#[test]
|
|
fn part1() {
|
|
let now = Instant::now();
|
|
println!("Test 2:");
|
|
let result = simulate_all(INPUT, 2, 2);
|
|
result
|
|
.iter()
|
|
.sorted_by(|a, b| a.0.cmp(b.0))
|
|
.for_each(|(saved, count)| {
|
|
println!("There are {count} cheats that saved {saved} picoseconds");
|
|
});
|
|
println!("Ran in {}", get_elapsed_string(now.elapsed()));
|
|
assert_eq!(result.values().copied().sum::<u32>(), 44);
|
|
}
|
|
|
|
#[test]
|
|
fn part2() {
|
|
let now = Instant::now();
|
|
println!("Test 2:");
|
|
let result = simulate_all(INPUT, 50, 20);
|
|
result
|
|
.iter()
|
|
.sorted_by(|a, b| a.0.cmp(b.0))
|
|
.for_each(|(saved, count)| {
|
|
println!("There are {count} cheats that saved {saved} picoseconds");
|
|
});
|
|
println!("Ran in {}", get_elapsed_string(now.elapsed()));
|
|
assert_eq!(result.values().copied().sum::<u32>(), 285);
|
|
}
|
|
}
|