From 59949a63ec0294f64faa5b5991a8387f0771fdbe Mon Sep 17 00:00:00 2001 From: Malte Tammena Date: Mon, 12 Dec 2022 20:39:38 +0100 Subject: [PATCH] Improve day12 performance, add tim's solution --- Cargo.lock | 66 +++++++++++++- Cargo.toml | 2 +- day12-tim/Cargo.toml | 8 ++ day12-tim/src/main.rs | 128 +++++++++++++++++++++++++++ day12/Cargo.toml | 2 + day12/src/height_map.rs | 130 ++++++++++++++++++++++++++++ day12/src/main.rs | 186 +++++----------------------------------- 7 files changed, 351 insertions(+), 171 deletions(-) create mode 100644 day12-tim/Cargo.toml create mode 100644 day12-tim/src/main.rs create mode 100644 day12/src/height_map.rs diff --git a/Cargo.lock b/Cargo.lock index f66821c..7e1eab2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,29 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf6ccdb167abbf410dcb915cabd428929d7f6a04980b54a11f26a39f1c7f7107" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + [[package]] name = "arrays" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dbdbb2877d26e0647c6b8125802b2ecf2dc2a28d864dde41e6f9b9a54da08fe" +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "beef" version = "0.5.2" @@ -158,9 +175,15 @@ name = "day12" version = "0.1.0" dependencies = [ "colorous", + "hashbrown 0.13.1", + "priority-queue", "termion", ] +[[package]] +name = "day12-tim" +version = "0.1.0" + [[package]] name = "day13" version = "0.1.0" @@ -247,6 +270,21 @@ dependencies = [ "wasi", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038" +dependencies = [ + "ahash", +] + [[package]] name = "heck" version = "0.4.0" @@ -262,6 +300,16 @@ dependencies = [ "libc", ] +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "io-lifetimes" version = "1.0.3" @@ -321,9 +369,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" [[package]] name = "linux-raw-sys" @@ -378,6 +426,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "priority-queue" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7685ca4cc0b3ad748c22ce6803e23b55b9206ef7715b965ebeaf41639238fdc" +dependencies = [ + "autocfg", + "indexmap", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -476,9 +534,9 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "rustix" -version = "0.36.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23" +checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" dependencies = [ "bitflags", "errno", diff --git a/Cargo.toml b/Cargo.toml index c9094dc..cb76fbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ "day01", "day01-gen-problem", "day02", "day02-gen-problem", "day03", "day03-gen-problem", "day04", "day05", "day06", "day07", "day08", "day09", "day10", "day11", "day12", "day13", "day14", "day15", "day16", "day17", "day18", "day19", "day20", "day21", "day22", "day23", "day24", ] +members = [ "day01", "day01-gen-problem", "day02", "day02-gen-problem", "day03", "day03-gen-problem", "day04", "day05", "day06", "day07", "day08", "day09", "day10", "day11", "day12", "day12-tim", "day13", "day14", "day15", "day16", "day17", "day18", "day19", "day20", "day21", "day22", "day23", "day24", ] [profile.release] #debug = true diff --git a/day12-tim/Cargo.toml b/day12-tim/Cargo.toml new file mode 100644 index 0000000..0672eff --- /dev/null +++ b/day12-tim/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "day12-tim" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/day12-tim/src/main.rs b/day12-tim/src/main.rs new file mode 100644 index 0000000..553e8d6 --- /dev/null +++ b/day12-tim/src/main.rs @@ -0,0 +1,128 @@ +use std::{ + fs::File, + io::{BufReader, BufRead, Error} +}; + +#[derive(Debug, Copy, Clone)] +struct Position { + row: usize, + col: usize +} +fn main() -> Result<(), Error> { + // You can pass the input filename as argument or otherwise + // it will use "input01.txt". + let file_name = ::std::env::args() + .skip(1) + .next() + .unwrap_or_else(|| String::from("./src/input01.txt")); + println!("Reading in '{}' and solving task 1 and 2.", file_name); + let reader = BufReader::new(File::open(file_name).expect("Input file not found!")); + + // Import the data and convert every character to an integer representation. + // a = 0, ... , z = 25. + const HEIGHT_OF_START_POSITION: i32 = 0; + const HEIGHT_OF_END_POSITION: i32 = 25; + const MAX_HEIGHT_WE_CAN_CLIMB: i32 = 1; + let mut start_position = Position {row: 0, col: 0}; + let mut end_position = Position {row: 0, col: 0}; + let data: Vec> = reader + .lines().enumerate() + .map(|(row, line)| line.unwrap().chars().enumerate() + .map(|(col, character)| { + if character == 'S' { + start_position.row = row; + start_position.col = col; + return HEIGHT_OF_START_POSITION; + } + else if character == 'E' { + end_position.row = row; + end_position.col = col; + return HEIGHT_OF_END_POSITION; + } + else { + return character as i32 - 'a' as i32; + } + }) + .collect()) + .collect(); + + // Get some informations about the input data. + let number_of_rows = data.len(); + let number_of_cols = data[0].len(); + + // Dijkstra. + // Originally i used an if statement to determine if we are solving part 1 or 2. + // Based on the task i set either the starting or the end point as initial position. + // But time measurement showed that backwards is much faster. + // So we will always start from the end. + let mut needed_steps: Vec> = + vec![vec![std::u32::MAX; number_of_cols]; number_of_rows]; + let mut visited_positions: Vec> = + vec![vec![false; number_of_cols]; number_of_rows]; + let mut unvisited_neighbours = Vec::::new(); + needed_steps[end_position.row][end_position.col] = 0; + visited_positions[end_position.row][end_position.col] = true; + unvisited_neighbours.push(end_position); + while unvisited_neighbours.len() > 0 { + // Get the position to use based on the lowest needed steps. + let index_current_position: Option = unvisited_neighbours + .iter() + .enumerate() + .min_by(|(_, pos1), (_, pos2)| + needed_steps[pos1.row][pos1.col].cmp(&needed_steps[pos2.row][pos2.col])) + .map(|(index, _)| index); + let index_current_position = index_current_position.unwrap(); + let current_row = unvisited_neighbours[index_current_position].row; + let current_col = unvisited_neighbours[index_current_position].col; + // Check what neighbours are reachable and if we can reach them faster than before. + // Therefore check the size of the map and also if we can reach the + // next step based on the height. + for look_vector in vec![vec![1,0],vec![0,1],vec![-1,0],vec![0,-1]] { + let look_vertical = look_vector[0]; + let look_horizontal = look_vector[1]; + let row = unvisited_neighbours[index_current_position].row as i32 + look_vertical; + let col = unvisited_neighbours[index_current_position].col as i32 + look_horizontal; + // Check size of the map. + // QUESTION: Can we take use of the auto bound check of rust? + if (row < 0) || (col < 0) || (row >= number_of_rows as i32) || (col >= number_of_cols as i32) { + continue; + } + let row = row as usize; + let col = col as usize; + // Check if we can reach it without climbing. We go backwards. + if data[current_row][current_col] <= data[row][col] + MAX_HEIGHT_WE_CAN_CLIMB { + // We can go there. Check if this is faster than before. + if needed_steps[row][col] > needed_steps[current_row][current_col] + 1 { + needed_steps[row][col] = needed_steps[current_row][current_col] + 1; + } + // If we have not been on this position before, put it on the list. + if visited_positions[row][col] == false { + unvisited_neighbours.push(Position{row: row, col: col}); + visited_positions[row][col] = true; + } + } + } + // We have now looked at all neighbours. Delete the current position from the vector + // and mark it as visited. + unvisited_neighbours.remove(index_current_position); + } + + // We got the 2d vector with all the distances from the end position. + // For task 1 we only need to look at the start position. + println!("Solution for task 1: {}", needed_steps[start_position.row][start_position.col]); + + // For task 2 we need to find the nearest position at height 0. + let result_task_2 = needed_steps + .iter() + .enumerate() + .flat_map(|(row, row_values)| row_values.iter().enumerate() + .filter(|&(col, _)| data[row][col] == 0) + .min_by_key(|&(_, value)| value)) + .min_by_key(|&(_, value)| value) + .map(|(_, value)| value) + .unwrap(); + println!("Solution for task 2: {}", result_task_2); + + + Ok(()) +} diff --git a/day12/Cargo.toml b/day12/Cargo.toml index a3ea9c9..c613ec4 100644 --- a/day12/Cargo.toml +++ b/day12/Cargo.toml @@ -9,4 +9,6 @@ app = true [dependencies] colorous = "1.0.8" +hashbrown = "0.13.1" +priority-queue = "1.3.0" termion = "2.0.1" diff --git a/day12/src/height_map.rs b/day12/src/height_map.rs new file mode 100644 index 0000000..107b179 --- /dev/null +++ b/day12/src/height_map.rs @@ -0,0 +1,130 @@ +use std::{cmp::Reverse, io::Read}; + +use hashbrown::hash_map::DefaultHashBuilder; +use priority_queue::PriorityQueue; + +#[derive(Debug)] +pub struct HeightMap { + heights: Vec, + width: usize, + height: usize, +} + +impl HeightMap { + pub fn find_shortest_path_len_to_end(&self) -> usize { + let mut iterations = 0; + let mut visited: Vec = self.heights.iter().map(|_| false).collect(); + let mut open: PriorityQueue, DefaultHashBuilder> = + PriorityQueue::with_hasher(DefaultHashBuilder::default()); + let end_idx = self + .heights + .iter() + .enumerate() + .find(|(_, &height)| height == b'E') + .unwrap() + .0; + open.push(end_idx, Reverse(0)); + while let Some((current_idx, Reverse(current_score))) = open.pop() { + iterations += 1; + if self.heights[current_idx] == TARGET { + eprintln!( + "Iter: {iterations} | {}x{} = {}", + self.width, + self.height, + self.height * self.width + ); + return current_score as usize; + } + visited[current_idx] = true; + for neighbor_idx in self.neighbors_of(current_idx) { + let tentative_score = current_score + 1; + if !visited[neighbor_idx] { + open.push(neighbor_idx, Reverse(tentative_score)); + } + } + } + panic!("Did not reach the target"); + } + + fn neighbors_of(&self, current: usize) -> impl Iterator + '_ { + let (x, y) = self.coords_of(current); + let curr_height = self.height_of(current); + [ + (x.saturating_sub(1), y), + (x, y.saturating_sub(1)), + (x + 1, y), + (x, y + 1), + ] + .into_iter() + .filter(move |&(x, _)| x < self.width) + .filter(|&(_, y)| y < self.height) + .filter(move |&(new_x, new_y)| new_x != x || new_y != y) + .map(|neigh| self.index_of(&neigh)) + .filter(move |&idx| self.height_of(idx) >= curr_height - 1) + } + + fn coords_of(&self, current: usize) -> (usize, usize) { + (current % self.width, current / self.width) + } + + fn index_of(&self, (x, y): &(usize, usize)) -> usize { + x + y * self.width + } + + fn height_of(&self, idx: usize) -> u8 { + match self.heights[idx] { + b'S' => b'a', + b'E' => b'z', + other => other, + } + } + + pub fn print_fancy(&self) { + let sup = (b'z' - b'a' + 1) as usize; + for y in 0..self.height { + for x in 0..self.width { + let idx = self.index_of(&(x, y)); + let height = self.height_of(idx) - b'a'; + let char = if self.heights[idx] == b'S' { + 'S' + } else if self.heights[idx] == b'E' { + 'E' + } else { + '█' + }; + let color = colorous::RED_YELLOW_GREEN.eval_rational(26 - height as usize, sup); + print!( + "{}{}{char}", + termion::color::Fg(termion::color::Rgb(color.r, color.g, color.b)), + termion::color::Bg(termion::color::Rgb(0, 0, 0)) + ); + } + println!() + } + println!() + } +} +impl From for HeightMap { + fn from(reader: R) -> Self { + let mut width = None; + let heights: Vec<_> = reader + .bytes() + .map(Result::unwrap) + .enumerate() + .inspect(|&(idx, byte)| { + if width.is_none() && byte == b'\n' { + width = Some(idx); + } + }) + .map(|(_, byte)| byte) + .filter(|&byte| byte != b'\n') + .collect(); + let width = width.unwrap_or(heights.len()); + let height = heights.len() / width; + HeightMap { + heights, + width, + height, + } + } +} diff --git a/day12/src/main.rs b/day12/src/main.rs index 0823f0e..3e01521 100644 --- a/day12/src/main.rs +++ b/day12/src/main.rs @@ -1,9 +1,12 @@ use std::{ - cmp::Reverse, fs::File, - io::{BufRead, BufReader, Read}, + io::{BufRead, BufReader}, }; +use height_map::HeightMap; + +mod height_map; + #[derive(Debug, Default, PartialEq, Eq)] enum SolvePuzzle { First, @@ -12,134 +15,9 @@ enum SolvePuzzle { Print, } -#[derive(Debug)] -struct HeightMap { - heights: Vec, - width: usize, - height: usize, -} - -#[derive(Debug, Eq, Ord)] -struct ScoredPos { - height: u8, - score: u32, - index_on_map: usize, -} - -impl HeightMap { - pub fn solve_a_star(&self) -> usize { - let mut entries: Vec<_> = self - .heights - .iter() - .enumerate() - .map(|(idx, &height)| ScoredPos { - height, - score: if height == STARTING { 0 } else { u32::MAX }, - index_on_map: idx, - }) - .collect(); - let mut open: Vec = entries - .iter() - .enumerate() - .filter(|(_, entry)| entry.height == STARTING) - .map(|(idx, _)| idx) - .collect(); - open.sort_by_cached_key(|&idx| Reverse(entries[idx].score)); - let mut came_from: Vec> = - ::std::iter::repeat(None).take(self.heights.len()).collect(); - while !open.is_empty() { - let current_idx = *open.last().unwrap(); - if entries[current_idx].height == b'E' { - return self.count_path(&came_from, current_idx); - } - let current_idx = open.pop().unwrap(); - for neighbor_idx in self.neighbors_of(current_idx) { - let tentative_score = &entries[current_idx].score + 1; - let neighbor = &mut entries[neighbor_idx]; - if tentative_score < neighbor.score { - came_from[neighbor_idx] = Some(current_idx); - neighbor.score = tentative_score; - if !open.contains(&neighbor_idx) { - open.push(neighbor_idx); - } - } - } - open.sort_by_cached_key(|&idx| Reverse(entries[idx].score)); - } - panic!() - } - - fn neighbors_of(&self, current: usize) -> impl Iterator + '_ { - let (x, y) = self.coords_of(current); - let curr_height = self.height_of(current); - [ - (x.saturating_sub(1), y), - (x, y.saturating_sub(1)), - (x + 1, y), - (x, y + 1), - ] - .into_iter() - .filter(move |&(x, _)| x < self.width) - .filter(|&(_, y)| y < self.height) - .filter(move |&(new_x, new_y)| new_x != x || new_y != y) - .map(|neigh| self.index_of(&neigh)) - .filter(move |&idx| self.height_of(idx) <= curr_height + 1) - } - - fn coords_of(&self, current: usize) -> (usize, usize) { - (current % self.width, current / self.width) - } - - fn index_of(&self, (x, y): &(usize, usize)) -> usize { - x + y * self.width - } - - fn height_of(&self, idx: usize) -> u8 { - match self.heights[idx] { - b'S' => b'a', - b'E' => b'z', - other => other, - } - } - - fn count_path(&self, came_from: &[Option], mut current: usize) -> usize { - let mut count = 0; - while let Some(new) = came_from[current] { - count += 1; - current = new; - } - count - } - - fn print_fancy(&self) { - let sup = (b'z' - b'a' + 1) as usize; - for y in 0..self.height { - for x in 0..self.width { - let idx = self.index_of(&(x, y)); - let height = self.height_of(idx) - b'a'; - let char = if self.heights[idx] == b'S' { - 'S' - } else if self.heights[idx] == b'E' { - 'E' - } else { - '█' - }; - let color = colorous::RED_YELLOW_GREEN.eval_rational(26 - height as usize, sup); - print!( - "{}{}{char}", - termion::color::Fg(termion::color::Rgb(color.r, color.g, color.b)), - termion::color::Bg(termion::color::Rgb(0, 0, 0)) - ); - } - println!() - } - println!() - } -} - fn solve_puzzle(reader: R) -> usize { let map = HeightMap::from(reader); - map.solve_a_star::() + map.find_shortest_path_len_to_end::() } fn print_puzzle(reader: R) -> usize { @@ -169,45 +47,21 @@ fn main() { println!("{}", solution); } -impl From for HeightMap { - fn from(reader: R) -> Self { - let mut width = None; - let heights: Vec<_> = reader - .bytes() - .map(Result::unwrap) - .enumerate() - .inspect(|&(idx, byte)| { - if width.is_none() && byte == b'\n' { - width = Some(idx); - } - }) - .map(|(_, byte)| byte) - .filter(|&byte| byte != b'\n') - .collect(); - let width = width.unwrap_or(heights.len()); - let height = heights.len() / width; - HeightMap { - heights, - width, - height, - } - } -} +#[cfg(test)] +mod tests { + use std::io::Cursor; -impl PartialEq for ScoredPos { - fn eq(&self, other: &Self) -> bool { - self.height == other.height - } -} + use super::*; -impl PartialOrd for ScoredPos { - fn partial_cmp(&self, other: &Self) -> Option { - if self.height < other.height { - Some(::std::cmp::Ordering::Less) - } else if self.height < other.height { - Some(::std::cmp::Ordering::Greater) - } else { - Some(::std::cmp::Ordering::Equal) - } + #[test] + fn example_on_first() { + let reader = Cursor::new(include_str!("../input.test")); + assert_eq!(solve_puzzle::(reader), 31); + } + + #[test] + fn example_on_second() { + let reader = Cursor::new(include_str!("../input.test")); + assert_eq!(solve_puzzle::(reader), 29); } }