Performance improvements for 2024 Quest 15

This commit is contained in:
Chris Alge 2024-11-24 17:15:07 +01:00
parent 7484de90f3
commit 6d665a8e71
2 changed files with 152 additions and 77 deletions

View file

@ -4,18 +4,22 @@ use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum ParseError { pub enum ParseError {
EmptyInput, EmptyInput,
GridTooBig,
NonRectangular, NonRectangular,
NoStart, NoStart,
ParseCharError(char), ParseCharError(char),
TooManyHerbs,
} }
impl Display for ParseError { impl Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::EmptyInput => write!(f, "Input doesn't contain a map"), 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::NonRectangular => write!(f, "Input is not rectangular"),
Self::NoStart => write!(f, "First line doesn't contain a walkable tile"), 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::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)] #[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Position { struct Position {
estimated_total_costs: u32, estimated_total_costs: u16,
costs: u32, costs: u16,
to_collect: usize, to_collect: u16,
coordinates: Coordinates, coordinates: u16,
collecting: u8, collecting: u8,
} }
struct Map { struct Map {
walkable: Vec<Vec<bool>>, walkable: Vec<Vec<bool>>,
herbs: HashMap<u8, Vec<Coordinates>>, herbs: Vec<Vec<u16>>,
width: usize, width: usize,
height: usize, height: usize,
start: Coordinates, start: Coordinates,
@ -43,7 +47,8 @@ impl TryFrom<&str> for Map {
type Error = ParseError; type Error = ParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> { fn try_from(value: &str) -> Result<Self, Self::Error> {
let mut herbs: HashMap<u8, Vec<Coordinates>> = HashMap::new(); let mut herb_ids: HashMap<char, usize> = HashMap::new();
let mut herbs: Vec<Vec<u16>> = Vec::new();
let mut walkable = Vec::new(); let mut walkable = Vec::new();
for (y, line) in value.lines().enumerate() { for (y, line) in value.lines().enumerate() {
let mut walkable_row = Vec::new(); let mut walkable_row = Vec::new();
@ -53,18 +58,30 @@ impl TryFrom<&str> for Map {
'#' | '~' => walkable_row.push(false), '#' | '~' => walkable_row.push(false),
l if l.is_ascii_uppercase() => { l if l.is_ascii_uppercase() => {
walkable_row.push(true); 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)), e => return Err(Self::Error::ParseCharError(e)),
} }
} }
walkable.push(walkable_row); walkable.push(walkable_row);
} }
if herbs.len() > 16 {
return Err(Self::Error::TooManyHerbs);
}
let height = walkable.len(); let height = walkable.len();
if height == 0 { if height == 0 {
return Err(Self::Error::EmptyInput); return Err(Self::Error::EmptyInput);
} }
let width = walkable[0].len(); let width = walkable[0].len();
if height > 0xFF && width > 0xFF {
return Err(Self::Error::GridTooBig);
}
if walkable.iter().any(|row| row.len() != width) { if walkable.iter().any(|row| row.len() != width) {
return Err(Self::Error::NonRectangular); return Err(Self::Error::NonRectangular);
} }
@ -74,35 +91,7 @@ impl TryFrom<&str> for Map {
} }
impl Map { impl Map {
fn route_single(&self, herb: u8) -> Option<u32> { fn route(&self, start: Coordinates, dest: Coordinates) -> Option<u16> {
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<u32> {
let mut open_set = VecDeque::from([(start, 0)]); let mut open_set = VecDeque::from([(start, 0)]);
let mut visited = HashSet::from([start]); let mut visited = HashSet::from([start]);
while let Some((pos, dist)) = open_set.pop_front() { while let Some((pos, dist)) = open_set.pop_front() {
@ -128,13 +117,84 @@ impl Map {
None None
} }
fn route_all(&self) -> Option<u32> { fn route_single(&self, herb_idx: usize) -> Option<u16> {
let interesting: Vec<(u8, Coordinates)> = self.herbs 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<u16> {
let start = ((self.start.0 << 8) + self.start.1) as u16;
let all_herbs = (1_u16 << self.herbs.len()) - 1;
let herbs_lut: HashMap<u16, u8> = self.herbs
.iter() .iter()
.flat_map(|(herb, coords)| coords.iter().cloned().map(|c| (*herb, c)).collect::<Vec<(u8, Coordinates)>>()) .enumerate()
.chain([(255, self.start)]) .flat_map(|(herb, coords)| coords.iter().map(|c| (*c, herb as u8)).collect::<Vec<_>>())
.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<u16> {
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::<Vec<(u8, u16)>>())
.chain([(255, start)])
.collect();
let network: HashMap<(u16, u16), u16> = interesting
.iter() .iter()
.enumerate() .enumerate()
.flat_map(|(l_idx, (l_herb, l_coords))| interesting .flat_map(|(l_idx, (l_herb, l_coords))| interesting
@ -142,30 +202,43 @@ impl Map {
.skip(l_idx + 1) .skip(l_idx + 1)
.filter(|(r_herb, _r_coords)| l_herb != r_herb) .filter(|(r_herb, _r_coords)| l_herb != r_herb)
.flat_map(|(_r_herb, r_coords)| { .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)] [((*l_coords, *r_coords), dist), ((*r_coords, *l_coords), dist)]
}).collect::<Vec<_>>()) }).collect::<Vec<_>>())
.collect(); .collect();
let all_herbs: Vec<_> = self.herbs.keys().cloned().collect(); let estimate: HashMap<(u16, u8), u16> = interesting
let all_herbs_int = (1_usize << all_herbs.len()) - 1; .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::<Vec<_>>()
).collect();
let all_herbs = (1_u16 << self.herbs.len()) - 1;
let mut open_set = BTreeSet::from([Position{ let mut open_set = BTreeSet::from([Position{
estimated_total_costs: 0, estimated_total_costs: 0,
costs: 0, costs: 0,
coordinates: self.start, coordinates: start,
to_collect: all_herbs_int, to_collect: all_herbs,
collecting: 0, collecting: 0,
}]); }]);
let mut visited = HashMap::new(); let mut visited = HashMap::new();
while let Some(pos) = open_set.pop_first() { while let Some(pos) = open_set.pop_first() {
if pos.to_collect == 0 { if pos.to_collect == 0 {
if pos.coordinates == self.start { if pos.coordinates == start {
return Some(pos.costs); return Some(pos.costs);
} else { } 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 { open_set.insert(Position {
estimated_total_costs: costs, estimated_total_costs: costs,
costs, costs,
coordinates: self.start, coordinates: start,
to_collect: 0, to_collect: 0,
collecting: 0, collecting: 0,
}); });
@ -177,36 +250,27 @@ impl Map {
) { ) {
visited.insert((pos.coordinates, pos.to_collect), pos.costs); 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)) { if let Some(remaining) = visited.get(&(pos.coordinates, collected)) {
open_set.insert(Position { open_set.insert(Position {
estimated_total_costs: pos.costs + remaining, estimated_total_costs: pos.costs + remaining,
costs: pos.costs + remaining, costs: pos.costs + remaining,
coordinates: self.start, coordinates: start,
to_collect: 0, to_collect: 0,
collecting: 0, collecting: 0,
}); });
} else { } else {
all_herbs (0..self.herbs.len())
.iter() .filter(|herb| pos.to_collect & (1_u16 << herb) != 0)
.enumerate() .for_each(|herb| {
.filter(|(idx, _herb)| pos.to_collect & (1_usize << idx) != 0) let to_collect = pos.to_collect & !(1_u16 << herb);
.for_each(|(idx, herb)| { self.herbs[herb]
let to_collect = pos.to_collect - (1_usize << idx);
self.herbs.get(herb).unwrap()
.iter() .iter()
.for_each(|&coordinates| { .for_each(|&coordinates| {
let costs = pos.costs + network.get(&(pos.coordinates, coordinates)).unwrap(); let costs = pos.costs + network.get(&(pos.coordinates, coordinates)).unwrap();
let estimated_total_costs = costs + all_herbs let estimated_total_costs = costs + (0..self.herbs.len())
.iter() .filter(|other_herb| to_collect & (1_u16 << other_herb) != 0)
.enumerate() .map(|other_herb| *estimate.get(&(coordinates, other_herb as u8)).unwrap())
.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))
.max() .max()
.unwrap_or(0); .unwrap_or(0);
@ -215,7 +279,7 @@ impl Map {
costs, costs,
coordinates, coordinates,
to_collect, to_collect,
collecting: idx as u8, collecting: herb as u8,
}); });
}); });
}); });
@ -228,11 +292,12 @@ impl Map {
} }
} }
pub fn run(input: &str, part: usize) -> Result<u32, ParseError> { pub fn run(input: &str, part: usize) -> Result<u16, ParseError> {
let map = Map::try_from(input)?; let map = Map::try_from(input)?;
match part { match part {
1 => Ok(2 * map.route_single(b'H' - b'A').unwrap()), 1 => Ok(2 * map.route_single(0).unwrap()),
2 | 3 => Ok(map.route_all().unwrap()), 2 => Ok(map.route_all_bfs().unwrap()),
3 => Ok(map.route_all_a_star().unwrap()),
_ => panic!("Illegal part number"), _ => panic!("Illegal part number"),
} }
} }
@ -248,7 +313,7 @@ mod tests {
#[test] #[test]
fn test_sample() { fn test_sample() {
let expected = [26, 38]; let expected = [26, 38, 38];
for part in 1..=expected.len() { for part in 1..=expected.len() {
let sample_input = read_file(&format!("tests/sample{part}")); let sample_input = read_file(&format!("tests/sample{part}"));
assert_eq!(run(&sample_input, part), Ok(expected[part-1])); assert_eq!(run(&sample_input, part), Ok(expected[part-1]));

View file

@ -0,0 +1,10 @@
##########.##########
#...................#
#.###.##.###.##.#.#.#
#..A#.#..~~~....#A#.#
#.#...#.~~~~~...#.#.#
#.#.#.#.~~~~~.#.#.#.#
#...#.#.B~~~B.#.#...#
#...#....BBB..#....##
#C............#....C#
#####################