Vastly improved solution for 2022/day19-part1
This commit is contained in:
parent
ede5141783
commit
0e1053ec5e
@ -1,5 +1,3 @@
|
||||
use std::{cmp::Ordering, collections::HashSet};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Blueprint {
|
||||
// The id and costs parsed from the input.
|
||||
@ -16,17 +14,16 @@ struct Blueprint {
|
||||
}
|
||||
|
||||
impl Blueprint {
|
||||
// Solve the given blueprint.
|
||||
fn solve(&mut self) {
|
||||
// Solve the given blueprint using BFS.
|
||||
fn solve_bfs(&mut self) {
|
||||
// For performance reasons we will search the solution space using breadth-first search.
|
||||
|
||||
// The HashSet for the next timeslot built in the current loop iteration.
|
||||
let mut next_hs: HashSet<RecursionState> = HashSet::new();
|
||||
// The HashSet *of* the current timeslot that is being processed.
|
||||
let mut hs: HashSet<RecursionState>;
|
||||
// vec_a has the RecursionStates for the current timeslot, while vec_b has the next slot's states.
|
||||
let mut vec_a: Vec<RecursionState> = Vec::with_capacity(2u64.pow(20) as usize);
|
||||
let mut vec_b: Vec<RecursionState> = Vec::with_capacity(2u64.pow(20) as usize);
|
||||
|
||||
// Initialize the simulation with the starting state.
|
||||
next_hs.insert(RecursionState {
|
||||
vec_a.push(RecursionState {
|
||||
ore_robots: 1,
|
||||
clay_robots: 0,
|
||||
obsidian_robots: 0,
|
||||
@ -35,23 +32,17 @@ impl Blueprint {
|
||||
clay: 0,
|
||||
obsidian: 0,
|
||||
geode: 0,
|
||||
remaining_time: 24,
|
||||
});
|
||||
|
||||
// Iterate over all timeslots.
|
||||
for ts in (1usize..=24).rev() {
|
||||
// Building a robot at t=1 cannot influence the final geode-count,
|
||||
// so it's omitted from the simulation here.
|
||||
for ts in (2usize..=24).rev() {
|
||||
// println!("Now at {} remaining time. Processing {} input RSs ...", ts, next_hs.len());
|
||||
|
||||
// Make the new HashSet the current one and
|
||||
// initialize a new one for this loop iteration.
|
||||
hs = next_hs;
|
||||
next_hs = HashSet::new();
|
||||
|
||||
let mut insert_count = 0usize;
|
||||
|
||||
// Process every RS of the past timeslot
|
||||
// to find all the states for the current timeslot.
|
||||
for rs in &hs {
|
||||
for rs in &vec_a {
|
||||
// Go through all five options and branch down them, if possible.
|
||||
// Specifically, we can either:
|
||||
// -> Build one of the four robot types, if resources permit.
|
||||
@ -59,152 +50,90 @@ impl Blueprint {
|
||||
// It's important to note that the optimal solution may include waiting in the middle,
|
||||
// i.e. letting resources accumulate so we can build one of the more expensive robots
|
||||
// down the line instead of immediately spending the resources on a cheaper robot type.
|
||||
|
||||
// Copy over the current state and let time for it pass.
|
||||
// This is the same no matter what type of robot we build since the robot will
|
||||
// go live at the end of the timeslot, not at its beginning.
|
||||
let mut next_rs = *rs;
|
||||
next_rs.ore += next_rs.ore_robots as u16;
|
||||
next_rs.clay += next_rs.clay_robots as u16;
|
||||
next_rs.obsidian += next_rs.obsidian_robots as u16;
|
||||
next_rs.geode += next_rs.geode_robots as u16;
|
||||
|
||||
// In branches (1) through (4) we copy the state, commission the robot, let time pass,
|
||||
// actually add the robot, and add the resulting state to the queue.
|
||||
// Check whether we can build the different robots, using `rs` and not `next_rs`
|
||||
// since the resources have to be allocated at the beginning of the turn.
|
||||
|
||||
// (1) Ore Robot
|
||||
if rs.ore >= self.ore_robot_ore_cost {
|
||||
let mut nrs = *rs;
|
||||
let mut nrs = next_rs;
|
||||
nrs.ore -= self.ore_robot_ore_cost;
|
||||
nrs.pass_time();
|
||||
nrs.ore_robots += 1;
|
||||
next_hs.insert(nrs);
|
||||
insert_count += 1;
|
||||
// checked_insert(&mut q, nrs);
|
||||
vec_b.push(nrs);
|
||||
}
|
||||
// (2) Clay Robot
|
||||
if rs.ore >= self.clay_robot_ore_cost {
|
||||
let mut nrs = *rs;
|
||||
let mut nrs = next_rs;
|
||||
nrs.ore -= self.clay_robot_ore_cost;
|
||||
nrs.pass_time();
|
||||
nrs.clay_robots += 1;
|
||||
next_hs.insert(nrs);
|
||||
insert_count += 1;
|
||||
// checked_insert(&mut q, nrs);
|
||||
vec_b.push(nrs);
|
||||
}
|
||||
// (3) Obsidian Robot
|
||||
if rs.ore >= self.obsidian_robot_ore_cost
|
||||
&& rs.clay >= self.obsidian_robot_clay_cost
|
||||
{
|
||||
let mut nrs = *rs;
|
||||
let mut nrs = next_rs;
|
||||
nrs.ore -= self.obsidian_robot_ore_cost;
|
||||
nrs.clay -= self.obsidian_robot_clay_cost;
|
||||
nrs.pass_time();
|
||||
nrs.obsidian_robots += 1;
|
||||
next_hs.insert(nrs);
|
||||
insert_count += 1;
|
||||
// checked_insert(&mut q, nrs);
|
||||
vec_b.push(nrs);
|
||||
}
|
||||
// (4) Geode Robot
|
||||
if rs.ore >= self.geode_robot_ore_cost
|
||||
&& rs.obsidian >= self.geode_robot_obsidian_cost
|
||||
{
|
||||
let mut nrs = *rs;
|
||||
let mut nrs = next_rs;
|
||||
nrs.ore -= self.geode_robot_ore_cost;
|
||||
nrs.obsidian -= self.geode_robot_obsidian_cost;
|
||||
nrs.pass_time();
|
||||
nrs.geode_robots += 1;
|
||||
next_hs.insert(nrs);
|
||||
insert_count += 1;
|
||||
// checked_insert(&mut q, nrs);
|
||||
vec_b.push(nrs);
|
||||
}
|
||||
// (5) Build nothing and let time pass.
|
||||
let mut nrs = *rs;
|
||||
nrs.pass_time();
|
||||
next_hs.insert(nrs);
|
||||
insert_count += 1;
|
||||
// checked_insert(&mut q, nrs);
|
||||
vec_b.push(next_rs);
|
||||
}
|
||||
|
||||
// Done!
|
||||
println!("Finished simulation round for t = {}", ts);
|
||||
println!(" inserted elements: {}", insert_count);
|
||||
println!(" initial next_hs.len(): {}", next_hs.len());
|
||||
println!(" inserted elements: {}", vec_b.len());
|
||||
|
||||
// For the last few timesteps, pruning isn't worth it. Just calculate the rest in that case.
|
||||
let worst_case = next_hs.len() * 5usize.pow(ts as u32);
|
||||
if worst_case > 100_000_000 && ts > 4 {
|
||||
// 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;
|
||||
// Prune elements.
|
||||
prune_states(&mut vec_b, &mut vec_a);
|
||||
println!(" elements after prune: {}", vec_a.len());
|
||||
|
||||
// Next up: Reduce the number of elements in the set using the Pareto optimality comparsion.
|
||||
println!("Pruning redundant elements ...");
|
||||
loop {
|
||||
let mut strictly_inferior_hs: HashSet<RecursionState> = 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 {
|
||||
// if 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::<HashSet<_>>();
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"{} calculations left / timeslot {} - no need to prune.",
|
||||
worst_case, ts
|
||||
);
|
||||
}
|
||||
println!(" final next_hs.len(): {}\n", next_hs.len());
|
||||
// Clear vec_b since all the relevant states have been copied over to vec_a.
|
||||
vec_b.clear();
|
||||
|
||||
// for e in &vec_a {
|
||||
// e.print();
|
||||
// println!();
|
||||
// }
|
||||
// println!();
|
||||
//
|
||||
// if ts == 15 {
|
||||
// std::process::exit(1);
|
||||
// }
|
||||
}
|
||||
|
||||
// Only RSs with no simulation-time should be left by now.
|
||||
assert!(next_hs.iter().all(|e| e.remaining_time == 0));
|
||||
|
||||
// Print and store the result.
|
||||
self.optimal_geode_count = next_hs.iter().map(|e| e.geode).max().unwrap();
|
||||
// Collect and print the final geode count.
|
||||
// Remember that we still have to simulate the geode-collection for t=1,
|
||||
// hence `e.geode + e.geode_robots as u16`.
|
||||
self.optimal_geode_count = vec_a.iter().map(|e| e.geode + e.geode_robots as u16).max().unwrap();
|
||||
println!("Found optimal geode count: {}", self.optimal_geode_count);
|
||||
}
|
||||
}
|
||||
|
||||
// Store all of the state that's passed up and down the recursion in one struct.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
// Derive PartialOrd + Ord for lexicographic sorting, something we'll use during pruning.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct RecursionState {
|
||||
// The currently active fleet of robots.
|
||||
ore_robots: u8,
|
||||
@ -216,124 +145,66 @@ struct RecursionState {
|
||||
clay: u16,
|
||||
obsidian: u16,
|
||||
geode: u16,
|
||||
// How much time is left in the simulation.
|
||||
remaining_time: u8,
|
||||
}
|
||||
|
||||
impl RecursionState {
|
||||
// Let one unit of time pass on this RecursionState.
|
||||
// Collects all the resources from the currently active robots.
|
||||
fn pass_time(&mut self) {
|
||||
self.ore += self.ore_robots as u16;
|
||||
self.clay += self.clay_robots as u16;
|
||||
self.obsidian += self.obsidian_robots as u16;
|
||||
self.geode += self.geode_robots as u16;
|
||||
// One unit of time passes.
|
||||
self.remaining_time -= 1;
|
||||
}
|
||||
|
||||
// Compare two recursion states.
|
||||
// This method essentially checks for Pareto improvements. An example:
|
||||
// RS1: 2 ore, 2 clay, 1 ore robot, 1 clay robot, 20 minutes left
|
||||
// RS2: 2 ore, 1 clay, 1 ore robot, 1 clay robot, 20 minutes left
|
||||
// RS1 is just as "good" in terms of ore, robot counts and time left
|
||||
// but is "strictly better" in terms of clay. It makes no sense to continue
|
||||
// running the simulation for RS2 b/c it cannot possibly produce a better
|
||||
// outcome than RS1.
|
||||
// Having more resources, robots or time can only ever lead to better outcomes.
|
||||
// If, however, RS1 had, say, one more ore but less clay than RS2 we cannot say
|
||||
// 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 {
|
||||
// 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 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);
|
||||
print!("{:>3} B, ", self.obsidian);
|
||||
print!("{:>3} G, ", self.geode);
|
||||
print!("{:>3} OR, ", self.ore_robots);
|
||||
print!("{:>3} CR, ", self.clay_robots);
|
||||
print!("{:>3} BR, ", self.obsidian_robots);
|
||||
print!("{:>3} GR, ", self.geode_robots);
|
||||
println!("{:>3} T", self.remaining_time);
|
||||
print!("{:>3} O, ", self.ore);
|
||||
print!("{:>3} C, ", self.clay);
|
||||
print!("{:>3} B, ", self.obsidian);
|
||||
print!("{:>3} G, ", self.geode);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy over states for the next simulation round, pruning a lot of (but not all)
|
||||
// RecursionStates that are "strictly inferior" in terms of Pareto optimality.
|
||||
// Will clear any previously present elements in dest.
|
||||
//
|
||||
// A few words on the general idea here:
|
||||
// The goal here is to check for Pareto improvements. An example:
|
||||
// RS1: 2 ore, 2 clay, 1 ore robot, 1 clay robot, 20 minutes left
|
||||
// RS2: 2 ore, 1 clay, 1 ore robot, 1 clay robot, 20 minutes left
|
||||
// RS1 is just as "good" in terms of ore, robot counts and time left
|
||||
// but is "strictly better" in terms of clay. It makes no sense to continue
|
||||
// running the simulation for RS2 b/c it cannot possibly produce a better
|
||||
// outcome than RS1.
|
||||
// Having more resources, robots or time can only ever lead to better outcomes.
|
||||
// If, however, RS1 had, say, one more ore but less clay than RS2 we cannot say
|
||||
// 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 solver.
|
||||
fn prune_states(source: &mut [RecursionState], dest: &mut Vec<RecursionState>) {
|
||||
// Begin by sorting the source lexicrgraphically and clearing the destination.
|
||||
source.sort_unstable();
|
||||
dest.clear();
|
||||
// Add one more element to the source, at the very end,
|
||||
// that is a copy of the last element +1 ore.
|
||||
// This is to ensure that the actual last element
|
||||
// Iterate through it from smallest to largest element and look at every pair of states.
|
||||
for (a, b) in source.iter().zip(source.iter().skip(1)) {
|
||||
// Don't copy a over if it is strictly inferior or equal to b.
|
||||
// Compare from bottom to top.
|
||||
if a.geode > b.geode
|
||||
|| a.obsidian > b.obsidian
|
||||
|| a.clay > b.clay
|
||||
|| a.ore > b.ore
|
||||
|| a.geode_robots > b.geode_robots
|
||||
|| a.obsidian_robots > b.obsidian_robots
|
||||
|| a.clay_robots > b.clay_robots
|
||||
|| a.ore_robots > b.ore_robots
|
||||
{
|
||||
dest.push(*a);
|
||||
}
|
||||
}
|
||||
// Copy over the very last element, guaranteed to be part of the frontier.
|
||||
dest.push(*source.last().unwrap());
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Use command line arguments to specify the input filename.
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
@ -366,7 +237,7 @@ fn main() {
|
||||
for bp in &mut blueprints {
|
||||
// Solve every blueprint with 24 minutes of time.
|
||||
println!("Solving Blueprint {}", bp.id);
|
||||
bp.solve();
|
||||
bp.solve_bfs();
|
||||
}
|
||||
|
||||
println!(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user