Improve day12 performance, add tim's solution

This commit is contained in:
Malte Tammena 2022-12-12 20:39:38 +01:00
parent 0e87cb94d3
commit 59949a63ec
7 changed files with 351 additions and 171 deletions

66
Cargo.lock generated
View file

@ -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",

View file

@ -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

8
day12-tim/Cargo.toml Normal file
View file

@ -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]

128
day12-tim/src/main.rs Normal file
View file

@ -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<Vec<i32>> = 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<u32>> =
vec![vec![std::u32::MAX; number_of_cols]; number_of_rows];
let mut visited_positions: Vec<Vec<bool>> =
vec![vec![false; number_of_cols]; number_of_rows];
let mut unvisited_neighbours = Vec::<Position>::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<usize> = 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(())
}

View file

@ -9,4 +9,6 @@ app = true
[dependencies]
colorous = "1.0.8"
hashbrown = "0.13.1"
priority-queue = "1.3.0"
termion = "2.0.1"

130
day12/src/height_map.rs Normal file
View file

@ -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<u8>,
width: usize,
height: usize,
}
impl HeightMap {
pub fn find_shortest_path_len_to_end<const TARGET: u8>(&self) -> usize {
let mut iterations = 0;
let mut visited: Vec<bool> = self.heights.iter().map(|_| false).collect();
let mut open: PriorityQueue<usize, Reverse<u32>, 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<Item = usize> + '_ {
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<R: Read> From<R> 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,
}
}
}

View file

@ -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<u8>,
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<const STARTING: u8>(&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<usize> = 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<Option<usize>> =
::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<Item = usize> + '_ {
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<usize>], 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<const STARTING: u8, R: BufRead>(reader: R) -> usize {
let map = HeightMap::from(reader);
map.solve_a_star::<STARTING>()
map.find_shortest_path_len_to_end::<STARTING>()
}
fn print_puzzle<R: BufRead>(reader: R) -> usize {
@ -169,45 +47,21 @@ fn main() {
println!("{}", solution);
}
impl<R: Read> From<R> 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<std::cmp::Ordering> {
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::<b'S', _>(reader), 31);
}
#[test]
fn example_on_second() {
let reader = Cursor::new(include_str!("../input.test"));
assert_eq!(solve_puzzle::<b'a', _>(reader), 29);
}
}