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)]
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<Vec<bool>>,
herbs: HashMap<u8, Vec<Coordinates>>,
herbs: Vec<Vec<u16>>,
width: usize,
height: usize,
start: Coordinates,
@ -43,7 +47,8 @@ impl TryFrom<&str> for Map {
type Error = ParseError;
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();
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<u32> {
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> {
fn route(&self, start: Coordinates, dest: Coordinates) -> Option<u16> {
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<u32> {
let interesting: Vec<(u8, Coordinates)> = self.herbs
fn route_single(&self, herb_idx: usize) -> 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_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()
.flat_map(|(herb, coords)| coords.iter().cloned().map(|c| (*herb, c)).collect::<Vec<(u8, Coordinates)>>())
.chain([(255, self.start)])
.enumerate()
.flat_map(|(herb, coords)| coords.iter().map(|c| (*c, herb as u8)).collect::<Vec<_>>())
.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()
.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::<Vec<_>>())
.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::<Vec<_>>()
).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<u32, ParseError> {
pub fn run(input: &str, part: usize) -> Result<u16, ParseError> {
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]));

View file

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