From 6d665a8e7169f906ad16ce6d972944080eb74b8d Mon Sep 17 00:00:00 2001 From: Chris Alge Date: Sun, 24 Nov 2024 17:15:07 +0100 Subject: [PATCH] Performance improvements for 2024 Quest 15 --- .../src/lib.rs | 219 ++++++++++++------ .../tests/sample3 | 10 + 2 files changed, 152 insertions(+), 77 deletions(-) create mode 100644 2024/day15_from_the_herbalists_diary/tests/sample3 diff --git a/2024/day15_from_the_herbalists_diary/src/lib.rs b/2024/day15_from_the_herbalists_diary/src/lib.rs index 590c778..be6b60c 100644 --- a/2024/day15_from_the_herbalists_diary/src/lib.rs +++ b/2024/day15_from_the_herbalists_diary/src/lib.rs @@ -4,18 +4,22 @@ use std::collections::{BTreeSet, HashMap, HashSet, VecDeque}; #[derive(Debug, PartialEq, Eq)] pub enum ParseError { EmptyInput, + GridTooBig, NonRectangular, NoStart, ParseCharError(char), + TooManyHerbs, } impl Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::EmptyInput => write!(f, "Input doesn't contain a map"), + Self::GridTooBig => write!(f, "Input map is too big. Maximum allowed size is 256x256."), Self::NonRectangular => write!(f, "Input is not rectangular"), Self::NoStart => write!(f, "First line doesn't contain a walkable tile"), Self::ParseCharError(e) => write!(f, "Unable to parse into a field: {e}"), + Self::TooManyHerbs => write!(f, "At most 16 herbs are supported"), } } } @@ -24,16 +28,16 @@ type Coordinates = (usize, usize); #[derive(PartialEq, Eq, PartialOrd, Ord)] struct Position { - estimated_total_costs: u32, - costs: u32, - to_collect: usize, - coordinates: Coordinates, + estimated_total_costs: u16, + costs: u16, + to_collect: u16, + coordinates: u16, collecting: u8, } struct Map { walkable: Vec>, - herbs: HashMap>, + herbs: Vec>, width: usize, height: usize, start: Coordinates, @@ -43,7 +47,8 @@ impl TryFrom<&str> for Map { type Error = ParseError; fn try_from(value: &str) -> Result { - let mut herbs: HashMap> = HashMap::new(); + let mut herb_ids: HashMap = HashMap::new(); + let mut herbs: Vec> = Vec::new(); let mut walkable = Vec::new(); for (y, line) in value.lines().enumerate() { let mut walkable_row = Vec::new(); @@ -53,18 +58,30 @@ impl TryFrom<&str> for Map { '#' | '~' => walkable_row.push(false), l if l.is_ascii_uppercase() => { walkable_row.push(true); - herbs.entry(l as u8 - b'A').and_modify(|v| v.push((x, y))).or_insert(Vec::from([(x, y)])); + let next_id = herbs.len(); + if let Some(&idx) = herb_ids.get(&l) { + herbs[idx].push(((x << 8) + y) as u16); + } else { + herb_ids.insert(l, next_id); + herbs.push(vec![((x << 8) + y) as u16]); + } }, e => return Err(Self::Error::ParseCharError(e)), } } walkable.push(walkable_row); } + if herbs.len() > 16 { + return Err(Self::Error::TooManyHerbs); + } let height = walkable.len(); if height == 0 { return Err(Self::Error::EmptyInput); } let width = walkable[0].len(); + if height > 0xFF && width > 0xFF { + return Err(Self::Error::GridTooBig); + } if walkable.iter().any(|row| row.len() != width) { return Err(Self::Error::NonRectangular); } @@ -74,35 +91,7 @@ impl TryFrom<&str> for Map { } impl Map { - fn route_single(&self, herb: u8) -> Option { - let mut open_set = VecDeque::from([(self.start, 0)]); - let mut visited = HashSet::from([self.start]); - if let Some(targets) = self.herbs.get(&herb) { - while let Some((pos, dist)) = open_set.pop_front() { - if targets.contains(&pos) { - return Some(dist); - } - [(0, 1), (2, 1), (1, 0), (1, 2)] - .iter() - .filter(|(dx, dy)| { - pos.0 + dx > 0 && - pos.1 + dy > 0 && - pos.0 + dx <= self.width && - pos.1 + dy <= self.height && - self.walkable[pos.1+dy-1][pos.0+dx-1] - }).for_each(|(dx, dy)| { - let next_pos = (pos.0+dx-1, pos.1+dy-1); - if !visited.contains(&next_pos) { - visited.insert(next_pos); - open_set.push_back((next_pos, dist+1)); - } - }); - } - } - None - } - - fn route(&self, start: Coordinates, dest: Coordinates) -> Option { + fn route(&self, start: Coordinates, dest: Coordinates) -> Option { let mut open_set = VecDeque::from([(start, 0)]); let mut visited = HashSet::from([start]); while let Some((pos, dist)) = open_set.pop_front() { @@ -128,13 +117,84 @@ impl Map { None } - fn route_all(&self) -> Option { - let interesting: Vec<(u8, Coordinates)> = self.herbs + fn route_single(&self, herb_idx: usize) -> Option { + let mut open_set = VecDeque::from([(self.start, 0)]); + let mut visited = HashSet::from([self.start]); + if let Some(targets) = self.herbs.get(herb_idx) { + let targets: Vec<_> = targets.iter().map(|coords| ((coords >> 8) as usize, (coords & 0xff) as usize)).collect(); + while let Some((pos, dist)) = open_set.pop_front() { + if targets.contains(&pos) { + return Some(dist); + } + [(0, 1), (2, 1), (1, 0), (1, 2)] + .iter() + .filter(|(dx, dy)| { + pos.0 + dx > 0 && + pos.1 + dy > 0 && + pos.0 + dx <= self.width && + pos.1 + dy <= self.height && + self.walkable[pos.1+dy-1][pos.0+dx-1] + }).for_each(|(dx, dy)| { + let next_pos = (pos.0+dx-1, pos.1+dy-1); + if !visited.contains(&next_pos) { + visited.insert(next_pos); + open_set.push_back((next_pos, dist+1)); + } + }); + } + } + None + } + + fn route_all_bfs(&self) -> Option { + let start = ((self.start.0 << 8) + self.start.1) as u16; + let all_herbs = (1_u16 << self.herbs.len()) - 1; + let herbs_lut: HashMap = self.herbs .iter() - .flat_map(|(herb, coords)| coords.iter().cloned().map(|c| (*herb, c)).collect::>()) - .chain([(255, self.start)]) + .enumerate() + .flat_map(|(herb, coords)| coords.iter().map(|c| (*c, herb as u8)).collect::>()) .collect(); - let network: HashMap<(Coordinates, Coordinates), u32> = interesting + let mut open_set = VecDeque::from([(start, all_herbs, 0)]); + let mut visited = HashSet::from([(start, all_herbs)]); + while let Some((pos, to_collect, dist)) = open_set.pop_front() { + let (x, y) = (pos >> 8, pos & 0xFF); + let to_collect = if let Some(herb) = herbs_lut.get(&pos) { + to_collect & !(1_u16 << herb) + } else { + to_collect + }; + + if to_collect == 0 && pos == start { + return Some(dist); + } + [(0, 1), (2, 1), (1, 0), (1, 2)] + .iter() + .filter(|(dx, dy)| { + x + dx > 0 && + y + dy > 0 && + x + dx <= self.width as u16 && + y + dy <= self.height as u16 && + self.walkable[(y+dy-1) as usize][(x+dx-1) as usize] + }).for_each(|(dx, dy)| { + let next_pos = pos + (dx << 8) + dy - 0x101; + if !visited.contains(&(next_pos, to_collect)) { + visited.insert((next_pos, to_collect)); + open_set.push_back((next_pos, to_collect, dist+1)); + } + }); + } + None + } + + fn route_all_a_star(&self) -> Option { + let start = ((self.start.0 << 8) + self.start.1) as u16; + let interesting: Vec<(u8, u16)> = self.herbs + .iter() + .enumerate() + .flat_map(|(herb, coords)| coords.iter().cloned().map(|c| (herb as u8, c)).collect::>()) + .chain([(255, start)]) + .collect(); + let network: HashMap<(u16, u16), u16> = interesting .iter() .enumerate() .flat_map(|(l_idx, (l_herb, l_coords))| interesting @@ -142,30 +202,43 @@ impl Map { .skip(l_idx + 1) .filter(|(r_herb, _r_coords)| l_herb != r_herb) .flat_map(|(_r_herb, r_coords)| { - let dist = self.route(*l_coords, *r_coords).unwrap(); + let dist = self.route(((l_coords >> 8) as usize, (l_coords & 0xFF) as usize), ((r_coords >> 8) as usize, (r_coords & 0xFF) as usize)).unwrap(); [((*l_coords, *r_coords), dist), ((*r_coords, *l_coords), dist)] }).collect::>()) .collect(); - let all_herbs: Vec<_> = self.herbs.keys().cloned().collect(); - let all_herbs_int = (1_usize << all_herbs.len()) - 1; + let estimate: HashMap<(u16, u8), u16> = interesting + .iter() + .flat_map(|(herb, coords)| self.herbs + .iter() + .enumerate() + .filter(|(other_herb, _)| *other_herb as u8 != *herb) + .map(|(other_herb, coords_vec)| ((*coords, other_herb as u8), coords_vec + .iter() + .map(|other_coords| network.get(&(*coords, *other_coords)).unwrap() + network.get(&(*other_coords, start)).unwrap()) + .min() + .unwrap()) + ).collect::>() + ).collect(); + + let all_herbs = (1_u16 << self.herbs.len()) - 1; let mut open_set = BTreeSet::from([Position{ estimated_total_costs: 0, costs: 0, - coordinates: self.start, - to_collect: all_herbs_int, + coordinates: start, + to_collect: all_herbs, collecting: 0, }]); let mut visited = HashMap::new(); while let Some(pos) = open_set.pop_first() { if pos.to_collect == 0 { - if pos.coordinates == self.start { + if pos.coordinates == start { return Some(pos.costs); } else { - let costs = pos.costs + network.get(&(pos.coordinates, self.start)).unwrap(); + let costs = pos.costs + network.get(&(pos.coordinates, start)).unwrap(); open_set.insert(Position { estimated_total_costs: costs, costs, - coordinates: self.start, + coordinates: start, to_collect: 0, collecting: 0, }); @@ -176,63 +249,55 @@ impl Map { (to_coll ^ pos.to_collect) & to_coll == 0 ) { visited.insert((pos.coordinates, pos.to_collect), pos.costs); - - let collected = all_herbs_int - pos.to_collect - (1 << pos.collecting); + + let collected = all_herbs & !(pos.to_collect | (1 << pos.collecting)); if let Some(remaining) = visited.get(&(pos.coordinates, collected)) { open_set.insert(Position { estimated_total_costs: pos.costs + remaining, costs: pos.costs + remaining, - coordinates: self.start, + coordinates: start, to_collect: 0, collecting: 0, }); } else { - all_herbs - .iter() - .enumerate() - .filter(|(idx, _herb)| pos.to_collect & (1_usize << idx) != 0) - .for_each(|(idx, herb)| { - let to_collect = pos.to_collect - (1_usize << idx); - self.herbs.get(herb).unwrap() + (0..self.herbs.len()) + .filter(|herb| pos.to_collect & (1_u16 << herb) != 0) + .for_each(|herb| { + let to_collect = pos.to_collect & !(1_u16 << herb); + self.herbs[herb] .iter() .for_each(|&coordinates| { let costs = pos.costs + network.get(&(pos.coordinates, coordinates)).unwrap(); - let estimated_total_costs = costs + all_herbs - .iter() - .enumerate() - .filter(|(idx, _herb)| to_collect & (1_usize << idx) != 0) - .map(|(_idx, herb)| self.herbs.get(herb).unwrap() - .iter() - .map(|&c| (c, network.get(&(coordinates, c)).unwrap())) - .min_by_key(|(_coords, dist)| *dist) - .unwrap() - ).map(|(c, d)| d + *network.get(&(c, self.start)).unwrap_or(&0)) + let estimated_total_costs = costs + (0..self.herbs.len()) + .filter(|other_herb| to_collect & (1_u16 << other_herb) != 0) + .map(|other_herb| *estimate.get(&(coordinates, other_herb as u8)).unwrap()) .max() .unwrap_or(0); - + open_set.insert(Position { estimated_total_costs, costs, coordinates, to_collect, - collecting: idx as u8, + collecting: herb as u8, }); }); }); } } - + } - + None } } -pub fn run(input: &str, part: usize) -> Result { +pub fn run(input: &str, part: usize) -> Result { let map = Map::try_from(input)?; match part { - 1 => Ok(2 * map.route_single(b'H' - b'A').unwrap()), - 2 | 3 => Ok(map.route_all().unwrap()), + 1 => Ok(2 * map.route_single(0).unwrap()), + 2 => Ok(map.route_all_bfs().unwrap()), + 3 => Ok(map.route_all_a_star().unwrap()), _ => panic!("Illegal part number"), } } @@ -248,7 +313,7 @@ mod tests { #[test] fn test_sample() { - let expected = [26, 38]; + let expected = [26, 38, 38]; for part in 1..=expected.len() { let sample_input = read_file(&format!("tests/sample{part}")); assert_eq!(run(&sample_input, part), Ok(expected[part-1])); diff --git a/2024/day15_from_the_herbalists_diary/tests/sample3 b/2024/day15_from_the_herbalists_diary/tests/sample3 new file mode 100644 index 0000000..b21ed40 --- /dev/null +++ b/2024/day15_from_the_herbalists_diary/tests/sample3 @@ -0,0 +1,10 @@ +##########.########## +#...................# +#.###.##.###.##.#.#.# +#..A#.#..~~~....#A#.# +#.#...#.~~~~~...#.#.# +#.#.#.#.~~~~~.#.#.#.# +#...#.#.B~~~B.#.#...# +#...#....BBB..#....## +#C............#....C# +#####################