use std::{collections::HashMap, fmt::Display, iter}; use glam::IVec2; use itertools::Itertools; use phf::phf_map; #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] enum Pad { Keypad, Numpad, } #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] enum Direction { Up, Down, Left, Right, } impl Direction { fn to_vec(self) -> IVec2 { match self { Direction::Up => IVec2::new(-1, 0), Direction::Down => IVec2::new(1, 0), Direction::Left => IVec2::new(0, -1), Direction::Right => IVec2::new(0, 1), } } } impl Display for Direction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Direction::Up => write!(f, "^"), Direction::Down => write!(f, "v"), Direction::Left => write!(f, "<"), Direction::Right => write!(f, ">"), } } } static NUMPAD_POSITIONS: phf::Map = phf_map! { '7' => IVec2::new(0, 0), '8' => IVec2::new(0, 1), '9' => IVec2::new(0, 2), '4' => IVec2::new(1, 0), '5' => IVec2::new(1, 1), '6' => IVec2::new(1, 2), '1' => IVec2::new(2, 0), '2' => IVec2::new(2, 1), '3' => IVec2::new(2, 2), '0' => IVec2::new(3, 1), 'A' => IVec2::new(3, 2), }; static KEYPAD_POSITIONS: phf::Map = phf_map! { '^' => IVec2::new(0, 1), 'A' => IVec2::new(0, 2), '<' => IVec2::new(1, 0), 'v' => IVec2::new(1, 1), '>' => IVec2::new(1, 2), }; fn parse(input: &str) -> Vec { input.lines().map(|l| l.to_string()).collect() } fn directions(pos1: &IVec2, pos2: &IVec2) -> Vec> { let diff = pos2 - pos1; let mut path_directions = vec![]; if diff.x > 0 { path_directions.extend((0..diff.x).map(|_| Direction::Down)); } else { path_directions.extend((0..diff.x.abs()).map(|_| Direction::Up)); } if diff.y > 0 { path_directions.extend((0..diff.y).map(|_| Direction::Right)); } else { path_directions.extend((0..diff.y.abs()).map(|_| Direction::Left)); } let reverse = path_directions.iter().rev().map(|d| d.to_owned()).collect(); if reverse == path_directions { vec![path_directions] } else { vec![path_directions, reverse] } } fn is_allowed_path(pos1: &IVec2, directions: &[Direction], banned_pos: &IVec2) -> bool { let mut pos = *pos1; for direction in directions { pos += direction.to_vec(); if pos == *banned_pos { return false; } } true } fn get_paths(pos1: &IVec2, pos2: &IVec2, banned_pos: &IVec2) -> Vec> { directions(pos1, pos2) .iter() .filter(|d| is_allowed_path(pos1, d, banned_pos)) .map(|d| d.to_owned()) .collect() } fn min_length(code: &str, pads: &[Pad], cache: &mut HashMap<(String, usize), usize>) -> usize { // credits: RubixDev if pads.is_empty() { return code.len(); } if let Some(val) = cache.get(&(code.to_string(), pads.len())) { return *val; } let result = std::iter::once('A') .chain(code.chars()) .tuple_windows() .map(|(start, end)| { match pads[0] { Pad::Numpad => get_paths( NUMPAD_POSITIONS.get(&start).unwrap(), NUMPAD_POSITIONS.get(&end).unwrap(), &IVec2::new(3, 0), ), Pad::Keypad => get_paths( KEYPAD_POSITIONS.get(&start).unwrap(), KEYPAD_POSITIONS.get(&end).unwrap(), &IVec2::new(0, 0), ), } .into_iter() .map(|dirs| dirs.iter().map(|d| d.to_string()).collect::() + "A") }) .multi_cartesian_product() .map(|combination| { combination .iter() .map(|c| min_length(c, &pads[1..], cache)) .sum::() }) .min() .unwrap(); cache.insert((code.to_string(), pads.len()), result); result } fn part12(input: &[String], keypad_robot_count: usize) -> usize { let mut cache = HashMap::new(); let pads: Vec = iter::once(Pad::Numpad) .chain((0..keypad_robot_count).map(|_| Pad::Keypad)) .collect(); input .iter() .map(|s| s[..s.len() - 1].parse::().unwrap() * min_length(s, &pads, &mut cache)) .sum() } fn main() { let input = parse(include_str!("../input.txt")); println!("{}", part12(&input, 2)); println!("{}", part12(&input, 25)); }