First solution for 2022/day19-part1, working but still improvable

This commit is contained in:
Tobias Marschner 2024-04-09 13:03:28 +02:00
parent 79c412297b
commit d71d669eb3

View File

@ -1,4 +1,7 @@
use std::{cmp::Ordering, collections::HashSet};
use std::{
cmp::Ordering,
collections::{HashSet, VecDeque},
};
#[derive(Debug)]
struct Blueprint {
@ -13,108 +16,140 @@ 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,
// 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 {
// Recursively solve a blueprint for the maximal number of geodes
// that can be found in the given time.
fn solve(&mut self, rs: &mut RecursionState) {
// Out of time? Return and check if we've improved the optimal result.
if rs.remaining_time == 0 {
if rs.geode > self.optimal_geode_count {
println!("New optimum for blueprint {}: {}", self.id, rs.geode);
self.optimal_geode_count = rs.geode;
// Solve the given blueprint.
fn solve(&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>;
// Initialize the simulation with the starting state.
next_hs.insert(RecursionState {
ore_robots: 1,
clay_robots: 0,
obsidian_robots: 0,
geode_robots: 0,
ore: 0,
clay: 0,
obsidian: 0,
geode: 0,
remaining_time: 24,
});
// Iterate over all timeslots.
for ts in (1usize..=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 {
// 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.
// -> Don't build anything at all.
// 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.
// 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.
// (1) Ore Robot
if rs.ore >= self.ore_robot_ore_cost {
let mut nrs = *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);
}
// (2) Clay Robot
if rs.ore >= self.clay_robot_ore_cost {
let mut nrs = *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);
}
// (3) Obsidian Robot
if rs.ore >= self.obsidian_robot_ore_cost
&& rs.clay >= self.obsidian_robot_clay_cost
{
let mut nrs = *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);
}
// (4) Geode Robot
if rs.ore >= self.geode_robot_ore_cost
&& rs.obsidian >= self.geode_robot_obsidian_cost
{
let mut nrs = *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);
}
// (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);
}
return;
// Done!
println!("Finished simulation round for t = {}", ts);
println!(" inserted elements: {}", insert_count);
println!(" initial next_hs.len(): {}", next_hs.len());
if next_hs.len() < 50_000 && ts > 4 {
// Next up: Reduce the number of elements in the set using the Pareto optimality comparsion.
println!("Removing redundant elements ...");
let mut reduced_hs: HashSet<RecursionState> = 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);
}
}
next_hs = reduced_hs;
println!(" done!");
} else {
println!("Number of states too large or timeslot too late. Not optimizing.");
}
println!(" final next_hs.len(): {}\n", next_hs.len());
}
// 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;
}
// Only RSs with no simulation-time should be left by now.
assert!(next_hs.iter().all(|e| e.remaining_time == 0));
// 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, and
// recurse, w/o letting any time pass.
// -> Don't build anything at all, but *do* let time pass.
// 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.
// (1) Ore Robot
if rs.ore >= self.ore_robot_ore_cost {
// Commission the robot, subtracting the resources.
rs.ore -= self.ore_robot_ore_cost;
// Let time pass.
rs.pass_time();
// Add the commissioned robot and recurse.
rs.ore_robots += 1;
self.solve(rs);
// "Unbuild" the robot in order to check the other possible timelines.
// First remove the robot, then "unpass" the time, and then re-add the resources.
rs.ore_robots -= 1;
rs.reverse_time();
rs.ore += self.ore_robot_ore_cost;
}
// (2) Clay Robot
if rs.ore >= self.clay_robot_ore_cost {
// Commission the robot, subtracting the resources.
rs.ore -= self.clay_robot_ore_cost;
// Let time pass.
rs.pass_time();
// Add the commissioned robot and recurse.
rs.clay_robots += 1;
self.solve(rs);
// "Unbuild" the robot in order to check the other possible timelines.
// First remove the robot, then "unpass" the time, and then re-add the resources.
rs.clay_robots -= 1;
rs.reverse_time();
rs.ore += self.clay_robot_ore_cost;
}
// (3) Obsidian Robot
if rs.ore >= self.obsidian_robot_ore_cost && rs.clay >= self.obsidian_robot_clay_cost {
// Commission the robot, subtracting the resources.
rs.ore -= self.obsidian_robot_ore_cost;
rs.clay -= self.obsidian_robot_clay_cost;
// Let time pass.
rs.pass_time();
// Add the commissioned robot and recurse.
rs.obsidian_robots += 1;
self.solve(rs);
// "Unbuild" the robot in order to check the other possible timelines.
// First remove the robot, then "unpass" the time, and then re-add the resources.
rs.obsidian_robots -= 1;
rs.reverse_time();
rs.ore += self.obsidian_robot_ore_cost;
rs.clay += self.obsidian_robot_clay_cost;
}
// (4) Geode Robot
if rs.ore >= self.geode_robot_ore_cost && rs.obsidian >= self.geode_robot_obsidian_cost {
// Commission the robot, subtracting the resources.
rs.ore -= self.geode_robot_ore_cost;
rs.obsidian -= self.geode_robot_obsidian_cost;
// Let time pass.
rs.pass_time();
// Add the commissioned robot and recurse.
rs.geode_robots += 1;
self.solve(rs);
// "Unbuild" the robot in order to check the other possible timelines.
// First remove the robot, then "unpass" the time, and then re-add the resources.
rs.geode_robots -= 1;
rs.reverse_time();
rs.ore += self.geode_robot_ore_cost;
rs.obsidian += self.geode_robot_obsidian_cost;
}
// (5) Build nothing and let time pass.
rs.pass_time();
self.solve(rs);
rs.reverse_time();
// Print and store the result.
self.optimal_geode_count = next_hs.iter().map(|e| e.geode).max().unwrap();
println!("Found optimal geode count: {}", self.optimal_geode_count);
}
}
@ -147,17 +182,6 @@ impl RecursionState {
self.remaining_time -= 1;
}
// Reverses the simulation by one unit of time.
// Of course, it is assumed that the active robots stay the same.
fn reverse_time(&mut self) {
self.ore -= self.ore_robots;
self.clay -= self.clay_robots;
self.obsidian -= self.obsidian_robots;
self.geode -= self.geode_robots;
// One unit of time is reserved.
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
@ -173,19 +197,20 @@ impl RecursionState {
// This comparison is used to cut off redundant simulation paths in the recursive solver.
fn compare(&self, other: &Self) -> Ordering {
// Collect the comparison result on all metrics.
let mut metrics: Vec<Ordering> = Vec::with_capacity(9);
// let mut metrics: Vec<Ordering> = Vec::with_capacity(9);
let mut metrics: [Ordering; 9] = [Ordering::Equal; 9];
// Resource counts
metrics.push(self.ore.cmp(&other.ore));
metrics.push(self.clay.cmp(&other.clay));
metrics.push(self.obsidian.cmp(&other.obsidian));
metrics.push(self.geode.cmp(&other.geode));
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.push(self.ore_robots.cmp(&other.ore_robots));
metrics.push(self.clay_robots.cmp(&other.clay_robots));
metrics.push(self.obsidian_robots.cmp(&other.obsidian_robots));
metrics.push(self.geode_robots.cmp(&other.geode_robots));
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.push(self.remaining_time.cmp(&other.remaining_time));
metrics[8] = self.remaining_time.cmp(&other.remaining_time);
// If one metric is strictly superior and the rest are better or equal,
// this RecursionState is "strictly better".
@ -214,80 +239,6 @@ impl RecursionState {
}
}
#[derive(Debug)]
struct ParetoFrontier {
data: Vec<HashSet<RecursionState>>,
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<String> = std::env::args().collect();
@ -313,29 +264,14 @@ fn main() {
geode_robot_ore_cost: l[27].parse::<usize>().unwrap(),
geode_robot_obsidian_cost: l[30].parse::<usize>().unwrap(),
optimal_geode_count: 0,
// Start with an empty ParetoFrontier.
pareto_frontier: ParetoFrontier::new(),
})
.collect::<Vec<_>>();
// Construct the starting recursion state.
let mut rs = RecursionState {
ore_robots: 1,
clay_robots: 0,
obsidian_robots: 0,
geode_robots: 0,
ore: 0,
clay: 0,
obsidian: 0,
geode: 0,
remaining_time: 24,
};
// Solve for every blueprint.
for bp in &mut blueprints {
// Solve every blueprint with 24 minutes of time.
println!("Solving Blueprint {}", bp.id);
bp.solve(&mut rs);
bp.solve();
}
println!(