First solution for 2022/day19-part1, working but still improvable
This commit is contained in:
parent
79c412297b
commit
d71d669eb3
@ -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!(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user