diff --git a/.gitea/workflows/build_and_run.yml b/.gitea/workflows/build_and_run.yml index 557f94f..4bef9ff 100644 --- a/.gitea/workflows/build_and_run.yml +++ b/.gitea/workflows/build_and_run.yml @@ -11,7 +11,7 @@ jobs: name: Challenge for day strategy: matrix: - day_number: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + day_number: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] runs-on: rust-bookworm steps: - name: Check out repository code diff --git a/day16/Cargo.toml b/day16/Cargo.toml new file mode 100644 index 0000000..b2016c1 --- /dev/null +++ b/day16/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "day16" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/day16/src/main.rs b/day16/src/main.rs new file mode 100644 index 0000000..74fe3f8 --- /dev/null +++ b/day16/src/main.rs @@ -0,0 +1,192 @@ +use std::cmp::Ordering; +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::{env, fs}; + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd)] +struct PositionData { + position: (i32, i32), + direction: (i32, i32), +} + +impl Ord for PositionData { + fn cmp(&self, other: &Self) -> Ordering { + self.position + .cmp(&other.position) + .then_with(|| self.direction.cmp(&other.direction)) + } +} + +impl From<(i32, i32)> for PositionData { + fn from(position: (i32, i32)) -> Self { + PositionData { + position, + direction: (0, 1), + } + } +} + +impl PositionData { + fn allowed_directions(&self) -> Vec<(u32, Self)> { + vec![ + ( + 0, + PositionData { + position: self.position, + direction: self.direction, + }, + ), + ( + 1000, + PositionData { + position: self.position, + direction: (-self.direction.1, self.direction.0), + }, + ), + ( + 1000, + PositionData { + position: self.position, + direction: (self.direction.1, -self.direction.0), + }, + ), + ] + } + + fn possible_directions(&self) -> Vec { + vec![ + PositionData { + position: self.position, + direction: self.direction, + }, + PositionData { + position: self.position, + direction: (-self.direction.0, -self.direction.1), + }, + PositionData { + position: self.position, + direction: (-self.direction.1, self.direction.0), + }, + PositionData { + position: self.position, + direction: (self.direction.1, -self.direction.0), + }, + ] + } + + fn position_forward(&self) -> Self { + PositionData { + position: ( + self.position.0 + self.direction.0, + self.position.1 + self.direction.1, + ), + direction: self.direction, + } + } +} + +fn read_input(path: &str) -> String { + fs::read_to_string(path).expect("Cannot read file.") +} + +fn parse_input(input: &str) -> (Vec>, (i32, i32), (i32, i32)) { + let mut map = Vec::new(); + let mut start = (0, 0); + let mut end = (0, 0); + for (row, line) in input.lines().filter(|l| !l.is_empty()).enumerate() { + map.push(Vec::new()); + for (col, char) in line.chars().enumerate() { + match char { + '#' => map.last_mut().unwrap().push(false), + '.' => map.last_mut().unwrap().push(true), + 'S' => { + map.last_mut().unwrap().push(true); + start = (row as i32, col as i32); + } + 'E' => { + map.last_mut().unwrap().push(true); + end = (row as i32, col as i32) + } + _ => unreachable!(), + } + } + } + + (map, start, end) +} + +fn walk_the_maze(map: &Vec>, start: &(i32, i32), end: &(i32, i32)) -> (usize, u32) { + let mut parents = HashMap::new(); + let mut scores = HashMap::new(); + let mut queue = BTreeSet::new(); + let mut end_score = None; + let start_position = PositionData::from(*start); + for (score, allowed_start_position) in start_position.allowed_directions() { + scores.insert(allowed_start_position, score); + queue.insert((score, allowed_start_position)); + } + + while let Some((original_score, original_position)) = queue.pop_first() { + let current_position = original_position.position_forward(); + let current_score = original_score + 1; + + if !map[current_position.position.0 as usize][current_position.position.1 as usize] { + continue; + } + + if current_score > *scores.get(¤t_position).unwrap_or(&u32::MAX) + || current_score > end_score.unwrap_or(u32::MAX) + { + continue; + } + + if current_position.position == *end && current_score < end_score.unwrap_or(u32::MAX) { + end_score = Some(current_score); + } + + for (score_offset, target_position) in current_position.allowed_directions() { + let target_score = current_score + score_offset; + let current_score = *scores.get(&target_position).unwrap_or(&u32::MAX); + if target_score <= current_score { + scores.insert(target_position, target_score); + queue.insert((target_score, target_position)); + if target_score < current_score { + parents.insert(target_position, HashSet::new()); + } + parents + .get_mut(&target_position) + .unwrap() + .insert(original_position); + } + } + } + + let mut unique_positions = HashSet::new(); + let end_positions = PositionData::from(*end).possible_directions(); + let mut queue: Vec<&PositionData> = end_positions + .iter() + .filter(|position_data| { + *scores.get(position_data).unwrap_or(&u32::MAX) == end_score.unwrap() + }) + .collect(); + while let Some(parent) = queue.pop() { + unique_positions.insert(parent.position); + let parents_positions = parents.get(&parent); + if parents_positions.is_some() { + queue.extend(parents_positions.unwrap().iter()); + } + } + + (unique_positions.len(), end_score.unwrap()) +} + +fn main() { + let args: Vec = env::args().collect(); + for arg in args.iter().skip(1) { + let input = read_input(&arg); + let (map, start, end) = parse_input(&input); + let (length, score) = walk_the_maze(&map, &start, &end); + println!("[{}]", &arg); + println!("\t[Part 1] => Answer is '{}'.", score); + println!("\t[Part 2] => Answer is '{}'.", length); + } +}