From 5045f83df8af36e8370b1f3bc0ecbaf4a59343dc Mon Sep 17 00:00:00 2001 From: Bert Peters Date: Sat, 28 Jan 2023 13:45:58 +0100 Subject: [PATCH] Slightly more efficient search --- 2022/src/day19.rs | 221 +++++++++++++++++++++++++++++++--------------- 1 file changed, 151 insertions(+), 70 deletions(-) diff --git a/2022/src/day19.rs b/2022/src/day19.rs index 2d1a1a6..835a968 100644 --- a/2022/src/day19.rs +++ b/2022/src/day19.rs @@ -1,7 +1,6 @@ +use std::array; use std::cmp::Ordering; use std::collections::BinaryHeap; -use std::ops::Add; -use std::ops::Sub; use anyhow::Result; use nom::bytes::complete::tag; @@ -45,81 +44,146 @@ impl TryFrom<&'_ [u8]> for Mineral { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -struct Resources([u8; 4]); - -impl Resources { - fn enough_for(self, other: Self) -> bool { - self.0.iter().zip(&other.0).all(|(a, b)| a >= b) - } -} - -impl Sub for Resources { - type Output = Self; - - fn sub(self, rhs: Self) -> Self::Output { - Self(std::array::from_fn(|i| self.0[i] - rhs.0[i])) - } -} - -impl Add<[u8; 4]> for Resources { - type Output = Self; - - fn add(self, rhs: [u8; 4]) -> Self::Output { - Self(std::array::from_fn(|i| self.0[i] + rhs[i])) - } -} - #[derive(Debug)] struct BluePrint { id: u32, - costs: [Resources; 4], + costs: [[u8; 3]; 4], } impl BluePrint { - pub fn max_geodes(&self, time: u32) -> u32 { - self.max_geodes_recursive(time, 0, [1, 0, 0, 0], Resources::default()) as u32 - } - - fn max_geodes_recursive( - &self, - time_left: u32, - // forbidden is a bitset for convenience - forbidden: u8, - machines: [u8; 4], - resources: Resources, - ) -> u8 { - if time_left <= 1 || forbidden.count_ones() == 4 { - return resources.0[3] + machines[3] * (time_left as u8); - } - - let resources_after = resources + machines; - - let mut best = 0; - - let mut can_buy = 0; - - for (i, &cost) in self.costs.iter().enumerate() { - if ((1 << i) & forbidden) == 0 && resources.enough_for(cost) { - can_buy |= 1 << i; - let mut new_machines = machines; - new_machines[i] += 1; - - best = best.max(self.max_geodes_recursive( - time_left - 1, - 0, - new_machines, - resources_after - cost, - )) + pub fn max_geodes(&self, time: u8) -> u8 { + /// How much would we produce if all we did was produce geode robots for the remaining time + fn ideal(remaining: u32) -> u32 { + if remaining <= 1 { + 0 + } else { + (remaining - 1) * remaining / 2 } } - best.max(self.max_geodes_recursive( - time_left - 1, - forbidden | can_buy, + #[derive(Eq, PartialEq)] + struct State { + missed: u32, + got: u8, + time_left: u8, + resources: [u8; 3], + machines: [u8; 3], + } + + impl Ord for State { + fn cmp(&self, other: &Self) -> Ordering { + Ordering::Equal + .then(other.missed.cmp(&self.missed)) + .then(self.got.cmp(&other.got)) + .then(self.time_left.cmp(&other.time_left)) + .then(self.machines.cmp(&other.machines)) + } + } + + impl PartialOrd for State { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + let max_needed = self.max_needed(); + let mut todo = BinaryHeap::new(); + let mut best = 0; + + todo.push(State { + missed: 0, + got: 0, + time_left: time, + resources: [0; 3], + machines: [1, 0, 0], + }); + + while let Some(State { + missed, + got, + time_left, + resources, machines, - resources_after, - )) + }) = todo.pop() + { + let ideal_from_now = ideal(time_left as u32); + if u32::from(best - got) > ideal_from_now { + continue; + } + if todo.len() > 1_000_000 { + panic!( + "Safety: got a todo list of len {}, best: {best}", + todo.len() + ); + } + 'element: for element in 0..4 { + let mut min_to_build = 0; + for ((&cost, &avail), &machine) in + self.costs[element].iter().zip(&resources).zip(&machines) + { + if cost > avail { + if machine == 0 { + continue 'element; + } else { + min_to_build = min_to_build.max((cost - avail + machine - 1) / machine); + } + } + } + + // +1 because we need a turn to build + let built_after = min_to_build + 1; + if built_after >= time_left { + continue; + } + + let resources_after = array::from_fn(|i| { + resources[i] + machines[i] * built_after - self.costs[element][i] + }); + let time_after = time_left - built_after; + + if element == Mineral::Geode as usize { + let new_got = got + time_after; + todo.push(State { + missed, + got: new_got, + time_left: time_after, + resources: resources_after, + machines, + }); + + best = best.max(new_got); + } else { + if machines[element] >= max_needed[element] { + continue; + } + + let mut new_machines = machines; + new_machines[element] += 1; + let new_missed = ideal_from_now - ideal(time_after as u32); + todo.push(State { + missed: new_missed, + got, + time_left: time_after, + resources: resources_after, + machines: new_machines, + }) + } + } + } + + best + } + + fn max_needed(&self) -> [u8; 3] { + let mut max_needed = [0; 3]; + + for cost in &self.costs { + for (max, &new) in max_needed.iter_mut().zip(cost) { + *max = (*max).max(new); + } + } + + max_needed } } @@ -137,7 +201,7 @@ fn parse_blueprint(input: &[u8]) -> IResult<&[u8], BluePrint> { let (mut input, id) = terminated(delimited(tag("Blueprint "), u32, tag(":")), multispace1)(input)?; - let mut costs: [Resources; 4] = Default::default(); + let mut costs: [[u8; 3]; 4] = Default::default(); let mut parse_robot = terminated( tuple(( @@ -152,10 +216,10 @@ fn parse_blueprint(input: &[u8]) -> IResult<&[u8], BluePrint> { let (remaining, (element, (amount1, req1), cost2)) = parse_robot(input)?; input = remaining; - costs[element as usize].0[req1 as usize] = amount1; + costs[element as usize][req1 as usize] = amount1; if let Some((amount2, req2)) = cost2 { - costs[element as usize].0[req2 as usize] = amount2; + costs[element as usize][req2 as usize] = amount2; } } @@ -190,8 +254,25 @@ mod tests { const SAMPLE: &[u8] = include_bytes!("./samples/19.txt"); + fn get_samples() -> Vec { + parse_input(SAMPLE, many1(parse_blueprint)).unwrap() + } + #[test] fn sample_part1() { + let samples = get_samples(); + + assert_eq!(samples[0].max_geodes(24), 9); + assert_eq!(samples[1].max_geodes(24), 12); + assert_eq!(part1(SAMPLE).unwrap(), "33"); } + + #[test] + fn sample_part2() { + let samples = get_samples(); + + assert_eq!(samples[0].max_geodes(32), 56); + assert_eq!(samples[1].max_geodes(32), 62); + } }