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)]
|
#[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()
|
.iter()
|
||||||
.flat_map(|(herb, coords)| coords.iter().cloned().map(|c| (*herb, c)).collect::<Vec<(u8, Coordinates)>>())
|
.filter(|(dx, dy)| {
|
||||||
.chain([(255, self.start)])
|
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()
|
||||||
|
.enumerate()
|
||||||
|
.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]));
|
||||||
|
|
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