advent-2022-rust/src/days/day12/mod.rs

299 lines
8 KiB
Rust

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<ResultType> {
let valley = Valley::try_from(lines)?;
Ok(ResultType::Integer(valley.walk()? as i64))
}
fn part2(&self, lines: &str) -> anyhow::Result<ResultType> {
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<usize>,
}
impl PartialOrd for Path {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
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<usize>) -> 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<Pos<usize>> {
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<Self::Item> {
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<Vec<char>>,
start: Pos<usize>,
exit: Pos<usize>,
width: usize,
}
impl TryFrom<&str> for Valley {
type Error = ValleyError;
fn try_from(lines: &str) -> Result<Self, Self::Error> {
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<usize>) -> char {
self.map[pos.y()][pos.x()]
}
fn do_walk<F>(&self, check: F) -> Result<usize, ValleyError>
where
F: Fn(Pos<usize>) -> 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(&current.pos) {
continue;
}
shortest.insert(current.pos);
for next in current.next_path(self) {
queue.push(next);
}
}
Err(ValleyError::NoPathFound)
}
pub fn walk(&self) -> Result<usize, ValleyError> {
self.do_walk(|pos| pos == self.start)
}
pub fn walk_short(&self) -> Result<usize, ValleyError> {
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(())
}
}