diff --git a/.gitea/workflows/build_and_run.yml b/.gitea/workflows/build_and_run.yml index 82e108b..5444adb 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, 16, 17, 18, 19, 20] + day_number: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] runs-on: rust-bookworm steps: - name: Check out repository code diff --git a/day21/Cargo.toml b/day21/Cargo.toml new file mode 100644 index 0000000..ed0c318 --- /dev/null +++ b/day21/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "day21" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/day21/src/button.rs b/day21/src/button.rs new file mode 100644 index 0000000..57d2eb5 --- /dev/null +++ b/day21/src/button.rs @@ -0,0 +1,188 @@ +pub trait Button: std::fmt::Debug + Eq { + fn forbidden_position(&self) -> (i32, i32); + fn to_position(&self) -> (i32, i32); + + fn distance_with(&self, other: &impl Button) -> Vec<(DirectionalButton, usize)> { + let source = self.to_position(); + let dest = other.to_position(); + let (dist_v, dist_h) = (dest.0 - source.0, dest.1 - source.1); + let (mul_v, mul_h) = (dist_v.abs(), dist_h.abs()); + + let mut movements = Vec::new(); + if mul_v > 0 { + movements.push(( + DirectionalButton::from_direction(&(dist_v / mul_v, 0)), + mul_v as usize, + )); + } + + if mul_h > 0 { + movements.push(( + DirectionalButton::from_direction(&(0, dist_h / mul_h)), + mul_h as usize, + )); + } + + movements + } + + fn possible_movements(&self, other: &impl Button) -> Vec> { + let movements = self.distance_with(other); + if movements.is_empty() { + return vec![vec![DirectionalButton::A, DirectionalButton::A]]; + } + + let source = self.to_position(); + let &first_move = movements.first().unwrap(); + let first_move_applied = first_move.0.apply_movement(&source, first_move.1 as i32); + let &last_move = movements.last().unwrap(); + let last_move_applied = last_move.0.apply_movement(&source, last_move.1 as i32); + let mut possible_movements = Vec::new(); + if first_move == last_move && first_move_applied != self.forbidden_position() { + let mut possibility = vec![DirectionalButton::A]; + possibility.extend(vec![first_move.0; first_move.1]); + possible_movements.push(possibility); + } else { + let mut first_possibility = vec![DirectionalButton::A]; + first_possibility.extend(vec![first_move.0; first_move.1]); + first_possibility.extend(vec![last_move.0; last_move.1]); + + let mut last_possibility = vec![DirectionalButton::A]; + last_possibility.extend(vec![last_move.0; last_move.1]); + last_possibility.extend(vec![first_move.0; first_move.1]); + + if first_move_applied != self.forbidden_position() + && last_move_applied != self.forbidden_position() + { + possible_movements.push(first_possibility); + possible_movements.push(last_possibility); + } else if first_move_applied != self.forbidden_position() + && last_move_applied == self.forbidden_position() + { + possible_movements.push(first_possibility); + } else if first_move_applied == self.forbidden_position() + && last_move_applied != self.forbidden_position() + { + possible_movements.push(last_possibility); + } else { + unreachable!() + } + } + + for possible_movement in possible_movements.iter_mut() { + possible_movement.push(DirectionalButton::A); + } + possible_movements + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum NumericButton { + Zero, + One, + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, + Nine, + A, +} + +impl From for NumericButton { + fn from(c: char) -> Self { + match c { + '0' => Self::Zero, + '1' => Self::One, + '2' => Self::Two, + '3' => Self::Three, + '4' => Self::Four, + '5' => Self::Five, + '6' => Self::Six, + '7' => Self::Seven, + '8' => Self::Eight, + '9' => Self::Nine, + 'A' => Self::A, + _ => unreachable!(), + } + } +} + +impl Button for NumericButton { + fn forbidden_position(&self) -> (i32, i32) { + (3, 0) + } + + fn to_position(&self) -> (i32, i32) { + match self { + Self::Seven => (0, 0), + Self::Eight => (0, 1), + Self::Nine => (0, 2), + Self::Four => (1, 0), + Self::Five => (1, 1), + Self::Six => (1, 2), + Self::One => (2, 0), + Self::Two => (2, 1), + Self::Three => (2, 2), + Self::Zero => (3, 1), + Self::A => (3, 2), + } + } +} + +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +pub enum DirectionalButton { + Up, + Down, + Left, + Right, + A, +} + +impl Button for DirectionalButton { + fn forbidden_position(&self) -> (i32, i32) { + (0, 0) + } + + fn to_position(&self) -> (i32, i32) { + match self { + Self::Up => (0, 1), + Self::A => (0, 2), + Self::Left => (1, 0), + Self::Down => (1, 1), + Self::Right => (1, 2), + } + } +} + +impl DirectionalButton { + fn from_direction(direction: &(i32, i32)) -> Self { + match direction { + (-1, 0) => Self::Up, + (1, 0) => Self::Down, + (0, 1) => Self::Right, + (0, -1) => Self::Left, + _ => unreachable!(), + } + } + + fn to_direction(&self) -> (i32, i32) { + match self { + Self::Up => (-1, 0), + Self::Down => (1, 0), + Self::Left => (0, -1), + Self::Right => (0, 1), + _ => unreachable!(), + } + } + + fn apply_movement(&self, position: &(i32, i32), times: i32) -> (i32, i32) { + let direction = self.to_direction(); + ( + position.0 + direction.0 * times, + position.1 + direction.1 * times, + ) + } +} diff --git a/day21/src/main.rs b/day21/src/main.rs new file mode 100644 index 0000000..08c8cef --- /dev/null +++ b/day21/src/main.rs @@ -0,0 +1,79 @@ +mod button; + +use crate::button::{Button, NumericButton}; +use std::collections::HashMap; +use std::{env, fs}; + +fn read_input(path: &str) -> String { + fs::read_to_string(path).expect("Cannot read file.") +} + +fn parse_input(input: &str) -> Vec<(usize, Vec)> { + let mut codes = Vec::new(); + for line in input.lines().filter(|l| !l.is_empty()) { + let mut buttons = vec![NumericButton::A]; + buttons.extend(line.chars().map(NumericButton::from)); + codes.push((line.strip_suffix("A").unwrap().parse().unwrap(), buttons)); + } + codes +} + +fn compute_keypad_movements( + buttons: &Vec, + cache: &mut Vec>, + depth: usize, +) -> usize { + let mut buttons_count = 0; + for button in buttons.windows(2) { + if cache[depth].contains_key(&(button[0].to_position(), button[1].to_position())) { + buttons_count += cache[depth] + .get(&(button[0].to_position(), button[1].to_position())) + .unwrap(); + } else { + let possible_moves = button[0].possible_movements(&button[1]); + let best_length; + if depth == 0 { + best_length = possible_moves + .iter() + .map(|moves| moves.len() - 1) + .min() + .unwrap(); + } else { + best_length = possible_moves + .iter() + .map(|possible_movement| { + compute_keypad_movements(&possible_movement, cache, depth - 1) + }) + .min() + .unwrap(); + } + buttons_count += best_length; + cache[depth].insert( + (button[0].to_position(), button[1].to_position()), + best_length, + ); + } + } + + buttons_count +} + +fn parts(codes: &Vec<(usize, Vec)>, depth: usize) -> usize { + codes + .iter() + .map(|(code, buttons)| { + code * compute_keypad_movements(buttons, &mut vec![HashMap::new(); depth], depth - 1) + }) + .sum() +} + +fn main() { + let args: Vec = env::args().collect(); + for arg in args.iter().skip(1) { + let input = read_input(&arg); + let codes = parse_input(&input); + println!("[{}]", &arg); + println!("\t[Part 1] => Answer is '{}'.", parts(&codes, 3)); + println!("\t[Part 2] => Answer is '{}'.", parts(&codes, 26)); + } +}