use colormap::ColorMap; use std::collections::hash_map::RandomState; use std::collections::{BinaryHeap, HashMap}; use std::fs::File; use std::io::{BufRead, BufReader, Lines, Write}; use std::iter::repeat; use std::time::Instant; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; mod colormap; const COLORMAP: &ColorMap = &colormap::COLORMAP_INFERNO; // BOILERPLATE type InputIter = Lines>; fn get_input() -> InputIter { let f = File::open("input").unwrap(); let br = BufReader::new(f); br.lines() } fn main() { let start = Instant::now(); let ans1 = problem1(get_input()); let duration = start.elapsed(); println!("Problem 1 solution: {} [{}s]", ans1, duration.as_secs_f64()); let start = Instant::now(); let ans2 = problem2(get_input()); let duration = start.elapsed(); println!("Problem 2 solution: {} [{}s]", ans2, duration.as_secs_f64()); } // DATA const UNPATH_CHAR: char = '█'; const UNVISITED_CHAR: char = ' '; #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] enum Direction { Left, Right, Up, Down, } impl Direction { const fn all() -> &'static [Self; 4] { &[ Direction::Left, Direction::Right, Direction::Up, Direction::Down, ] } const fn opposite(&self) -> Self { match self { Direction::Left => Direction::Right, Direction::Right => Direction::Left, Direction::Up => Direction::Down, Direction::Down => Direction::Up, } } } impl From<&Direction> for char { fn from(dir: &Direction) -> Self { match dir { Direction::Left => '←', Direction::Right => '→', Direction::Up => '↑', Direction::Down => '↓', } } } struct CityMap { map: Vec>, } impl CityMap { fn offset_pos(&self, pos: (usize, usize), dir: &Direction) -> Option<(usize, usize)> { match dir { Direction::Left if pos.0 > 0 => Some((pos.0 - 1, pos.1)), Direction::Right if pos.0 < self.map[0].len() - 1 => Some((pos.0 + 1, pos.1)), Direction::Up if pos.1 > 0 => Some((pos.0, pos.1 - 1)), Direction::Down if pos.1 < self.map.len() - 1 => Some((pos.0, pos.1 + 1)), _ => None, } } #[allow(dead_code)] fn print(&self) -> Result<(), Box> { let cost_max = *self.map.iter().flat_map(|row| row.iter()).max().unwrap() as f64; let mut so_lock = StandardStream::stdout(ColorChoice::Always); for y in 0..self.map.len() { for val in &self.map[y] { so_lock.set_color( ColorSpec::new() .set_bg(Some(COLORMAP.apply(*val as f64 / cost_max))) .set_fg(Some(Color::Black)), )?; so_lock.write_fmt(format_args!("{}", val))?; } so_lock.reset()?; writeln!(so_lock)?; } Ok(()) } } type Position = (usize, usize); struct WalkCost<'a> { start: Position, cost_from: Vec>>, map: &'a CityMap, } #[derive(Debug)] struct Move { new_pos: Position, dir: &'static Direction, consecutive: usize, weight: u64, } impl PartialEq for Move { fn eq(&self, other: &Self) -> bool { self.weight == other.weight } } impl Eq for Move {} impl PartialOrd for Move { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Move { fn cmp(&self, other: &Self) -> std::cmp::Ordering { std::cmp::Reverse(self.weight).cmp(&std::cmp::Reverse(other.weight)) } } impl<'a> WalkCost<'a> { fn from_map(map: &'a CityMap, start: Position) -> Self { Self { map, start, cost_from: map .map .iter() .map(|row| repeat(HashMap::new()).take(row.len()).collect()) .collect(), } } fn compute(&mut self) { let mut unvisited_next_moves: BinaryHeap = BinaryHeap::new(); let valid_start_moves: Vec = Direction::all() .iter() .filter_map(|dir| { self.map.offset_pos(self.start, dir).and_then(|pos| { Some(Move { new_pos: pos, dir, consecutive: 1, weight: self.map.map[pos.1][pos.0], }) }) }) // valid positions .collect(); for m in valid_start_moves { unvisited_next_moves.push(m); } while let Some(cur_move) = unvisited_next_moves.pop() { // we've been here already at a lower cost // if cur_move.weight >= self.cost_to[cur_move.new_pos.1][cur_move.new_pos.0] { // continue; // } if let Some(last_weight) = self.cost_from[cur_move.new_pos.1][cur_move.new_pos.0] .get(&(*cur_move.dir, cur_move.consecutive)) { if cur_move.weight < *last_weight { self.cost_from[cur_move.new_pos.1][cur_move.new_pos.0] .insert((*cur_move.dir, cur_move.consecutive), cur_move.weight); } else { continue; } // visited before at lower cost } } else { self.cost_from[cur_move.new_pos.1][cur_move.new_pos.0] .insert((*cur_move.dir, cur_move.consecutive), cur_move.weight); } // println!("state at {:?}: {:?}", cur_move, self.cost_from[cur_move.new_pos.1][cur_move.new_pos.0]); let valid_moves = Direction::all().iter().filter_map(|dir| { self.map .offset_pos(cur_move.new_pos, dir) .and_then(|new_pos| { Some(Move { new_pos, dir, consecutive: if cur_move.dir == dir { cur_move.consecutive + 1 } else { 1 }, weight: cur_move.weight + self.map.map[new_pos.1][new_pos.0], }) }) .filter(|m| m.consecutive != 4 && *m.dir != cur_move.dir.opposite()) .filter(|m| { m.weight < *self.cost_from[m.new_pos.1][m.new_pos.0] .get(&(*m.dir, m.consecutive)) .unwrap_or(&u64::MAX) }) }); // valid positions // println!("valid moves with {:?}", cur_move); for next_move in valid_moves { // println!(" {:?}", next_move); unvisited_next_moves.push(next_move); } } } } struct WalkCost2<'a> { start: Position, cost_from: Vec>>, map: &'a CityMap, } impl<'a> WalkCost2<'a> { fn from_map(map: &'a CityMap, start: Position) -> Self { Self { map, start, cost_from: map .map .iter() .map(|row| repeat(HashMap::new()).take(row.len()).collect()) .collect(), } } fn min_cost_at(&self, pos: Position) -> Option<&u64> { self.cost_from[pos.1][pos.0].values().min() } fn compute(&mut self, to: Position) { let mut unvisited_next_moves: BinaryHeap = BinaryHeap::new(); let valid_start_moves: Vec = Direction::all() .iter() .filter_map(|dir| { self.map.offset_pos(self.start, dir).and_then(|pos| { Some(Move { new_pos: pos, dir, consecutive: 1, weight: self.map.map[pos.1][pos.0], }) }) }) // valid positions .collect(); for m in valid_start_moves { unvisited_next_moves.push(m); } while let Some(cur_move) = unvisited_next_moves.pop() { // we've been here already at a lower cost // if cur_move.weight >= self.cost_to[cur_move.new_pos.1][cur_move.new_pos.0] { // continue; // } if let Some(last_weight) = self.cost_from[cur_move.new_pos.1][cur_move.new_pos.0] .get(&(*cur_move.dir, cur_move.consecutive)) { if cur_move.weight < *last_weight { self.cost_from[cur_move.new_pos.1][cur_move.new_pos.0] .insert((*cur_move.dir, cur_move.consecutive), cur_move.weight); // println!("move {:?} inserted {:?}", cur_move, (*cur_move.dir, cur_move.consecutive)); } else { continue; } // visited before at lower cost } } else { // println!("move {:?} inserted {:?}", cur_move, (*cur_move.dir, cur_move.consecutive)); self.cost_from[cur_move.new_pos.1][cur_move.new_pos.0] .insert((*cur_move.dir, cur_move.consecutive), cur_move.weight); } if cur_move.new_pos == to { // println!("reached end pos {:?} via {:?}", to, cur_move); continue; } let valid_moves = Direction::all().iter().filter_map(|dir| { self.map .offset_pos(cur_move.new_pos, dir) .and_then(|new_pos| { Some(Move { new_pos, dir, consecutive: if cur_move.dir == dir { cur_move.consecutive + 1 } else { 1 }, weight: cur_move.weight + self.map.map[new_pos.1][new_pos.0], }) }) .filter(|m| m.new_pos != self.start) .filter(|m| *m.dir != cur_move.dir.opposite()) .filter(|m| { if m.dir == cur_move.dir { m.consecutive < 11 } else { cur_move.consecutive >= 4 } }) .filter(|m| m.new_pos != to || m.consecutive >= 4) .filter(|m| { m.weight < *self.cost_from[m.new_pos.1][m.new_pos.0] .get(&(*m.dir, m.consecutive)) .unwrap_or(&u64::MAX) }) }); // valid positions // println!("valid moves with {:?}", cur_move); for next_move in valid_moves { // println!(" {:?}", next_move); unvisited_next_moves.push(next_move); } } } fn shortest_path_to(&self, to: Position) -> Vec<(Position, Direction)> { let mut path = Vec::new(); let mut cur_pos = to; // start at the end, walk backwards while cur_pos != self.start { let (m, _val) = self.cost_from[cur_pos.1][cur_pos.0] .iter() .min_by(|a, b| a.1.cmp(b.1)) .unwrap(); path.push((cur_pos, m.0)); cur_pos = self.map.offset_pos(cur_pos, &m.0.opposite()).unwrap(); } path } fn print_path(&self, to: Position) -> Result<(), Box> { let path = self.shortest_path_to(to); let map: HashMap<_, _, RandomState> = HashMap::from_iter(path.into_iter()); let cost_max_of_min = *self .cost_from .iter() .flat_map(|row| row.iter().filter_map(|cell| cell.values().min())) .max() .unwrap() as f64; let mut so_lock = StandardStream::stdout(ColorChoice::Always); for y in 0..self.cost_from.len() { for x in 0..self.map.map[y].len() { let mut color = ColorSpec::new(); let c = if let Some(to_dir) = map.get(&(x, y)) { let normalized_cost = *self.min_cost_at((x, y)).unwrap() as f64 / cost_max_of_min; let bg_color = COLORMAP.apply(normalized_cost); let fg_color = if let Color::Rgb(r, g, b) = bg_color { Color::Rgb(255 - r, 255 - g, 255 - b) } else { Color::Black }; color.set_fg(Some(fg_color)).set_bg(Some(bg_color)).bold(); to_dir.into() } else { if let Some(cost) = &self.min_cost_at((x, y)) { color.set_fg(Some(COLORMAP.apply(**cost as f64 / cost_max_of_min))); UNPATH_CHAR } else { color.set_fg(Some(Color::Black)); UNVISITED_CHAR } }; so_lock.set_color(&color)?; let mut char_buf = [0u8; 4]; c.encode_utf8(&mut char_buf); so_lock.write_all(&char_buf)?; } so_lock.reset()?; writeln!(so_lock)?; } Ok(()) } } impl From> for CityMap { fn from(lines: Lines) -> Self { Self { map: lines .map(|l| l.unwrap().chars().map(|c| c as u64 - '0' as u64).collect()) .collect(), } } } // PROBLEM 1 solution fn problem1(input: Lines) -> u64 { let map = CityMap::from(input); let mut costs = WalkCost::from_map(&map, (0, 0)); costs.compute(); // println!("{}", costs); // costs.print_shortest_path((costs.cost_to[0].len() - 1, costs.cost_to.len() - 1)); *costs.cost_from[costs.cost_from.len() - 1][costs.cost_from[0].len() - 1] .values() .min() .unwrap() } // PROBLEM 2 solution fn problem2(input: Lines) -> u64 { let map = CityMap::from(input); // map.print().unwrap(); let mut costs = WalkCost2::from_map(&map, (0, 0)); costs.compute((map.map[0].len() - 1, map.map.len() - 1)); // println!("{}", costs); costs .print_path((costs.cost_from[0].len() - 1, costs.cost_from.len() - 1)) .unwrap(); *costs.cost_from[costs.cost_from.len() - 1][costs.cost_from[0].len() - 1] .values() .min() .unwrap() } #[cfg(test)] mod tests { use crate::*; use std::io::Cursor; use test_case::test_case; const EXAMPLE: &str = &"2413432311323 3215453535623 3255245654254 3446585845452 4546657867536 1438598798454 4457876987766 3637877979653 4654967986887 4564679986453 1224686865563 2546548887735 4322674655533"; const EXAMPLE2: &str = &"111111111111 999999999991 999999999991 999999999991 999999999991"; #[test] fn problem1_example() { let c = Cursor::new(EXAMPLE); assert_eq!(problem1(c.lines()), 102); } #[test_case(EXAMPLE, 94)] #[test_case(EXAMPLE2, 71)] fn problem2_example(example: &str, expect: u64) { let c = Cursor::new(example); assert_eq!(problem2(c.lines()), expect); } }