diff --git a/2022/day19-part1/src/main.rs b/2022/day19-part1/src/main.rs index 126f781..aab0646 100644 --- a/2022/day19-part1/src/main.rs +++ b/2022/day19-part1/src/main.rs @@ -1,7 +1,4 @@ -use std::{ - cmp::Ordering, - collections::{HashSet, VecDeque}, -}; +use std::{cmp::Ordering, collections::HashSet}; #[derive(Debug)] struct Blueprint { @@ -125,22 +122,71 @@ impl Blueprint { println!(" inserted elements: {}", insert_count); println!(" initial next_hs.len(): {}", next_hs.len()); - if next_hs.len() < 50_000 && ts > 4 { + // For the last few timesteps, pruning isn't worth it. Just calculate the rest in that case. + if ts > 3 { + // Pruning runs in a nested loop over the elements of the set. + // In the inner loop we collect strictly inferior elements until we've met a + // threshold of the original count. + let initial_elems = next_hs.len() as f32; + // If the inner loop has found strictly inferior elements making up 10% of the + // original count, exit out of it and actually remove them before continuing to + // prune. + const INNER_LOOP_CUTOFF: f32 = 0.1; + // Count the total number of comparisons performed. + let mut comp_count = 0usize; + // This is the cutoff. Once we've reached this number of comparisons, we quit. + const TOTAL_COMP_COUNT: usize = 400_000_000usize; + // Next up: Reduce the number of elements in the set using the Pareto optimality comparsion. - println!("Removing redundant elements ..."); - let mut reduced_hs: HashSet = HashSet::new(); - for rs in &next_hs { - // Carry over rs if it is part of the HS's Pareto frontier. - if next_hs.iter().all(|e| rs.compare(e).is_ge()) { - reduced_hs.insert(*rs); + println!("Pruning redundant elements ..."); + loop { + let mut strictly_inferior_hs: HashSet = HashSet::new(); + for ars in &next_hs { + // Run a nested loop and mark any strictly inferior elements. + // Also keep track of whether ars may be part of the frontier. + for brs in &next_hs { + comp_count += 1; + match ars.compare(brs) { + Ordering::Greater => { + strictly_inferior_hs.insert(*brs); + } + Ordering::Less => { + strictly_inferior_hs.insert(*ars); + } + Ordering::Equal => (), + } + } + // Have we found enough elements yet? + // Or have we reached the limit in terms of comparisons? + if (strictly_inferior_hs.len() as f32) / initial_elems >= INNER_LOOP_CUTOFF + || comp_count >= TOTAL_COMP_COUNT + { + break; + } + } + // No more inferior elements? Then we're done. + if strictly_inferior_hs.is_empty() { + println!("No more inferior elements. Fully pruned."); + println!("comp_count: {}", comp_count); + break; + } else { + // Actually remove the strictly inferior elements from next_hs. + next_hs = next_hs + .difference(&strictly_inferior_hs) + .cloned() + .collect::>(); + // And reset the strictly_inferior set. + strictly_inferior_hs.clear(); + // If we've hit the limit in terms of no. of comparsions, just move on. + if comp_count >= TOTAL_COMP_COUNT { + println!("Comparison limit reached. Continuing."); + break; + } } } - next_hs = reduced_hs; - println!(" done!"); } else { - println!("Number of states too large or timeslot too late. Not optimizing."); + println!("Final 3 - no more pruning."); } - println!(" final next_hs.len(): {}\n", next_hs.len()); } @@ -195,37 +241,82 @@ impl RecursionState { // that RS1 is "strictly better". It is different, having made a different tradeoff in // resource collection, which may or may not lead to a better outcome overall. // This comparison is used to cut off redundant simulation paths in the recursive solver. + #[allow(clippy::comparison_chain)] fn compare(&self, other: &Self) -> Ordering { - // Collect the comparison result on all metrics. - // let mut metrics: Vec = Vec::with_capacity(9); - let mut metrics: [Ordering; 9] = [Ordering::Equal; 9]; - // Resource counts - metrics[0] = self.ore.cmp(&other.ore); - metrics[1] = self.clay.cmp(&other.clay); - metrics[2] = self.obsidian.cmp(&other.obsidian); - metrics[3] = self.geode.cmp(&other.geode); - // Robot counts - metrics[4] = self.ore_robots.cmp(&other.ore_robots); - metrics[5] = self.clay_robots.cmp(&other.clay_robots); - metrics[6] = self.obsidian_robots.cmp(&other.obsidian_robots); - metrics[7] = self.geode_robots.cmp(&other.geode_robots); - // Remaining time - metrics[8] = self.remaining_time.cmp(&other.remaining_time); + // This used to be a lot more idiomatic. + // However, to help improve performance, it now looks the way it does. + let mut less = false; + let mut greater = false; - // If one metric is strictly superior and the rest are better or equal, - // this RecursionState is "strictly better". - if metrics.iter().all(|e| e.is_ge()) && metrics.iter().any(|e| e.is_gt()) { - Ordering::Greater - // If one metric is strictly inferior and the rest are inferior or equal, - // this RecursionState is "strictly inferior". - } else if metrics.iter().all(|e| e.is_le()) && metrics.iter().any(|e| e.is_lt()) { - Ordering::Less - // In all other cases no definitive statement can be made. - } else { + if self.ore < other.ore { + less = true; + } else if self.ore > other.ore { + greater = true; + } + + if self.clay < other.clay { + less = true; + } else if self.clay > other.clay { + greater = true; + } + + if self.obsidian < other.obsidian { + less = true; + } else if self.obsidian > other.obsidian { + greater = true; + } + + if self.geode < other.geode { + less = true; + } else if self.geode > other.geode { + greater = true; + } + + // Short-circuit. + if less && greater { + return Ordering::Equal; + } + + if self.ore_robots < other.ore_robots { + less = true; + } else if self.ore_robots > other.ore_robots { + greater = true; + } + + if self.clay_robots < other.clay_robots { + less = true; + } else if self.clay_robots > other.clay_robots { + greater = true; + } + + if self.obsidian_robots < other.obsidian_robots { + less = true; + } else if self.obsidian_robots > other.obsidian_robots { + greater = true; + } + + if self.geode_robots < other.geode_robots { + less = true; + } else if self.geode_robots > other.geode_robots { + greater = true; + } + + if self.remaining_time < other.remaining_time { + less = true; + } else if self.remaining_time > other.remaining_time { + greater = true; + } + + if (!less && !greater) || (less && greater) { Ordering::Equal + } else if less { + Ordering::Less + } else { + Ordering::Greater } } + #[allow(dead_code)] fn print(&self) { print!("{:>3} O, ", self.ore); print!("{:>3} C, ", self.clay);