diff --git a/2022/day19-part1/src/main.rs b/2022/day19-part1/src/main.rs index 78f3936..62b3c6d 100644 --- a/2022/day19-part1/src/main.rs +++ b/2022/day19-part1/src/main.rs @@ -1,4 +1,4 @@ -use std::cmp::Ordering; +use std::{cmp::Ordering, collections::HashSet}; #[derive(Debug)] struct Blueprint { @@ -13,11 +13,9 @@ struct Blueprint { // The maximal number of geodes that can be collected by this blueprint. // Initialized to 0 and overwritten by the solver, once it concludes. optimal_geode_count: usize, - // For the sake of cutting out redundant simulation paths keep track of the - // "best" RecursionState for each timeslot. - // This way, when a RecursionState is "strictly inferior" to a simulation we ran in the past, - // we know we don't have to bother with this one and can simply return. - optimal_resursion_states: [RecursionState; 24], + // In order to optimize the simulation we keep track of all solutions' Pareto frontier. + // This allows us to cut out redundant simulation paths. + pareto_frontier: ParetoFrontier, } impl Blueprint { @@ -33,24 +31,12 @@ impl Blueprint { return; } - // Compare the current RecursionState at this time with the best RecursionState - // we've previously run. - // If it's equal, keep going, should be worthwhile. - // If it's strictly superior, store it and definitely keep going. - // If it's strictly inferior, there's no point in continuing, return now. - match rs.compare(&self.optimal_resursion_states[24 - rs.remaining_time]) { - Ordering::Equal => (), - Ordering::Greater => { - self.optimal_resursion_states[24 - rs.remaining_time] = *rs; - println!("New optimal RS for time {}.", rs.remaining_time); - for rs in &self.optimal_resursion_states { - print!("RS[{:>2}]: ", 24 - rs.remaining_time); - rs.print(); - } - } - Ordering::Less => { - return; - } + // Process the current RS in the ParetoFrontier. + // If it is strictly inferior to an element already in the set + // we've processed something better before already, and there is no point in continuing. + let res = self.pareto_frontier.update(rs); + if res.is_lt() { + return; } // Go through all five options and branch down them, if possible. @@ -133,7 +119,7 @@ impl Blueprint { } // Store all of the state that's passed up and down the recursion in one struct. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] struct RecursionState { // The currently active fleet of robots. ore_robots: usize, @@ -228,6 +214,80 @@ impl RecursionState { } } +#[derive(Debug)] +struct ParetoFrontier { + data: Vec>, + count: usize, +} + +impl ParetoFrontier { + fn new() -> ParetoFrontier { + // Create the new PF. + let mut pf = ParetoFrontier { + data: Vec::new(), + count: 0, + }; + // Initialize the 24 empty HashSets for each of the timeslots. + for _ in 0..24 { + pf.data.push(HashSet::new()); + } + pf + } + + // Process a new RecursionState within the frontier. + // If the RS is strictly inferior to one of the elements in the frontier, + // the frontier remains unchanged and Ordering::Less is returned. + // If the RS is "equal" to *all* elements in the frontier, + // Ordering::Equal is returned and it is added to the frontier. + // If the RS is strictly superior to one or more elements in the frontier, + // those elements will be removed from the frontier, RS will be added, + // and Ordering::Greater will be returned. + fn update(&mut self, rs: &RecursionState) -> Ordering { + // Compute the timeslot index. + let idx = 24 - rs.remaining_time; + // Run through (possibly all) elements of the current timeslot's frontier. + let mut result = Ordering::Equal; + for e in &self.data[idx] { + // Compare the current element in the frontier. + match rs.compare(e) { + Ordering::Equal => (), + Ordering::Less => { + // rs is strictly inferior to one of the set's elements. + // We're done here. + result = Ordering::Less; + break; + } + Ordering::Greater => { + // rs is striclty superior to one of the set's elements. + // We can technically already return, but we need to get rid of all elements in + // the set that rs is striclty superior to. + result = Ordering::Greater; + break; + } + } + } + // We've reached the end of the loop, or broke out of it early. + // If we've found a strictly superior element, perform cleanup now. + if result.is_gt() { + // Expensive, but presumably not invoked *too* often. + self.data[idx].retain(|e| e.compare(rs).is_eq()); + } + // If we've found an element that isn't strictly inferior, add it to the frontier. + if result.is_ge() { + self.data[idx].insert(*rs); + } + if self.count % 10000 == 0 { + for (i, hs) in self.data.iter().enumerate() { + println!("Entries for t={}: {}", i, hs.len()); + } + println!(); + } + self.count += 1; + // Return the loop's conclusion. + result + } +} + fn main() { // Use command line arguments to specify the input filename. let args: Vec = std::env::args().collect(); @@ -253,19 +313,8 @@ fn main() { geode_robot_ore_cost: l[27].parse::().unwrap(), geode_robot_obsidian_cost: l[30].parse::().unwrap(), optimal_geode_count: 0, - // Auto-fill the optimal RecrusionState-array with a dummy "terrible" RecursionState - // which will get overridden as the simulation progresses. - optimal_resursion_states: [RecursionState { - ore_robots: 0, - clay_robots: 0, - obsidian_robots: 0, - geode_robots: 0, - ore: 0, - clay: 0, - obsidian: 0, - geode: 0, - remaining_time: 0, - }; 24], + // Start with an empty ParetoFrontier. + pareto_frontier: ParetoFrontier::new(), }) .collect::>();