299 lines
8 KiB
Rust
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(¤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<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(())
|
|
}
|
|
}
|