Performance improvements for 2024 Quest 15
This commit is contained in:
parent
7484de90f3
commit
6d665a8e71
2 changed files with 152 additions and 77 deletions
|
@ -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]));
|
||||
|
|
10
2024/day15_from_the_herbalists_diary/tests/sample3
Normal file
10
2024/day15_from_the_herbalists_diary/tests/sample3
Normal file
|
@ -0,0 +1,10 @@
|
|||
##########.##########
|
||||
#...................#
|
||||
#.###.##.###.##.#.#.#
|
||||
#..A#.#..~~~....#A#.#
|
||||
#.#...#.~~~~~...#.#.#
|
||||
#.#.#.#.~~~~~.#.#.#.#
|
||||
#...#.#.B~~~B.#.#...#
|
||||
#...#....BBB..#....##
|
||||
#C............#....C#
|
||||
#####################
|
Loading…
Add table
Add a link
Reference in a new issue