use std::collections::{BinaryHeap, HashSet}; use crate::common::{file::split_lines, pos::Pos}; use super::template::{DayTrait, ResultType}; use thiserror::Error; const DAY_NUMBER: usize = 12; pub struct Day; impl DayTrait for Day { fn get_day_number(&self) -> usize { DAY_NUMBER } fn part1(&self, lines: &str) -> anyhow::Result { let valley = Valley::try_from(lines)?; Ok(ResultType::Integer(valley.walk()? as i64)) } fn part2(&self, lines: &str) -> anyhow::Result { let valley = Valley::try_from(lines)?; Ok(ResultType::Integer(valley.walk_short()? as i64)) } } #[derive(Debug, Error)] enum ValleyError { #[error("Not a legal terrain char: {0}")] NotALegalCharacter(char), #[error("Valley needs to be rectangle")] NotAReactangleValley, #[error("Valley map conatins no data")] EmptyValley, #[error("Could not find start point")] NoStartFound, #[error("Could not find exit point")] NoExitFound, #[error("No path found")] NoPathFound, } #[derive(Debug, PartialEq, Eq)] struct Path { length: usize, height: char, pos: Pos, } impl PartialOrd for Path { fn partial_cmp(&self, other: &Self) -> Option { match other.length.partial_cmp(&self.length) { Some(core::cmp::Ordering::Equal) => {} ord => return ord, } match other.height.partial_cmp(&self.height) { Some(core::cmp::Ordering::Equal) => {} ord => return ord, } match self.pos.x().partial_cmp(&other.pos.x()) { Some(core::cmp::Ordering::Equal) => {} ord => return ord, } self.pos.y().partial_cmp(&other.pos.y()) } } impl Ord for Path { fn cmp(&self, other: &Self) -> std::cmp::Ordering { other .length .cmp(&self.length) .then_with(|| other.height.cmp(&self.height)) .then_with(|| self.pos.x().cmp(&other.pos.x())) .then_with(|| self.pos.y().cmp(&other.pos.y())) } } impl Path { pub fn new(length: usize, height: char, pos: Pos) -> Self { Path { length, height, pos, } } pub fn next_path<'a>(&'a self, valley: &'a Valley) -> Neighbors<'a> { Neighbors::new(self, valley) } } struct Neighbors<'a> { path: &'a Path, valley: &'a Valley, state: usize, } impl<'a> Neighbors<'a> { pub fn new(path: &'a Path, valley: &'a Valley) -> Self { Neighbors { path, state: 0, valley, } } fn next_pos(&mut self) -> Option> { while self.state < 4 { self.state += 1; match self.state { 1 => { if self.path.pos.x() < self.valley.width() - 1 { return Some(Pos::new(self.path.pos.x() + 1, self.path.pos.y())); } } 2 => { if self.path.pos.y() > 0 { return Some(Pos::new(self.path.pos.x(), self.path.pos.y() - 1)); } } 3 => { if self.path.pos.x() > 0 { return Some(Pos::new(self.path.pos.x() - 1, self.path.pos.y())); } } 4 => { if self.path.pos.y() < self.valley.height() - 1 { return Some(Pos::new(self.path.pos.x(), self.path.pos.y() + 1)); } } _ => {} } } None } } impl Iterator for Neighbors<'_> { type Item = Path; fn next(&mut self) -> Option { while let Some(pos) = self.next_pos() { let height = self.valley.get_height(pos); if height as u32 + 1 >= self.path.height as u32 { return Some(Path::new(self.path.length + 1, height, pos)); } } None } } struct Valley { map: Vec>, start: Pos, exit: Pos, width: usize, } impl TryFrom<&str> for Valley { type Error = ValleyError; fn try_from(lines: &str) -> Result { let lines = split_lines(lines); let mut map = Vec::new(); let mut start = None; let mut exit = None; let mut valley_width = None; for (y, row) in lines.enumerate() { let mut height_row = Vec::new(); for (x, height_char) in row.chars().enumerate() { match height_char { 'S' => { start = Some(Pos::new(x, y)); height_row.push('a') } 'E' => { exit = Some(Pos::new(x, y)); height_row.push('z') } 'a'..='z' => height_row.push(height_char), _ => return Err(ValleyError::NotALegalCharacter(height_char)), } } if let Some(width) = valley_width { if width != height_row.len() { return Err(ValleyError::NotAReactangleValley); } } else { valley_width = Some(height_row.len()); } map.push(height_row); } let Some(width) = valley_width else { return Err(ValleyError::EmptyValley); }; let Some(start) = start else { return Err(ValleyError::NoStartFound); }; let Some(exit) = exit else { return Err(ValleyError::NoExitFound); }; Ok(Valley { map, start, exit, width, }) } } impl Valley { fn get_height(&self, pos: Pos) -> char { self.map[pos.y()][pos.x()] } fn do_walk(&self, check: F) -> Result where F: Fn(Pos) -> bool, { let mut shortest = HashSet::with_capacity(self.width * self.map.len()); let mut queue = BinaryHeap::new(); queue.push(Path::new(0, 'z', self.exit)); while let Some(current) = queue.pop() { if check(current.pos) { return Ok(current.length); } if shortest.contains(¤t.pos) { continue; } shortest.insert(current.pos); for next in current.next_path(self) { queue.push(next); } } Err(ValleyError::NoPathFound) } pub fn walk(&self) -> Result { self.do_walk(|pos| pos == self.start) } pub fn walk_short(&self) -> Result { self.do_walk(|pos| self.get_height(pos) == 'a') } #[inline] fn width(&self) -> usize { self.width } #[inline] fn height(&self) -> usize { self.map.len() } } #[cfg(test)] mod test { use super::*; use crate::common::file::read_string; use anyhow::Result; #[test] fn test_parse() -> Result<()> { let day = Day {}; let lines = read_string(day.get_day_number(), "example01.txt")?; let valley = Valley::try_from(lines.as_str())?; assert_eq!(valley.width, 8); assert_eq!(valley.start, Pos::new(0, 0)); assert_eq!(valley.exit, Pos::new(5, 2)); Ok(()) } #[test] fn test_part1() -> Result<()> { let day = Day {}; let lines = read_string(day.get_day_number(), "example01.txt")?; let expected = ResultType::Integer(31); let result = day.part1(&lines)?; assert_eq!(result, expected); Ok(()) } #[test] fn test_part2() -> Result<()> { let day = Day {}; let lines = read_string(day.get_day_number(), "example01.txt")?; let expected = ResultType::Integer(29); let result = day.part2(&lines)?; assert_eq!(result, expected); Ok(()) } }