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)]
|
#[derive(Debug)]
|
||||||
struct Blueprint {
|
struct Blueprint {
|
||||||
@ -13,108 +16,140 @@ struct Blueprint {
|
|||||||
// The maximal number of geodes that can be collected by this blueprint.
|
// The maximal number of geodes that can be collected by this blueprint.
|
||||||
// Initialized to 0 and overwritten by the solver, once it concludes.
|
// Initialized to 0 and overwritten by the solver, once it concludes.
|
||||||
optimal_geode_count: usize,
|
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 {
|
impl Blueprint {
|
||||||
// Recursively solve a blueprint for the maximal number of geodes
|
// Solve the given blueprint.
|
||||||
// that can be found in the given time.
|
fn solve(&mut self) {
|
||||||
fn solve(&mut self, rs: &mut RecursionState) {
|
// For performance reasons we will search the solution space using breadth-first search.
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the current RS in the ParetoFrontier.
|
// The HashSet for the next timeslot built in the current loop iteration.
|
||||||
// If it is strictly inferior to an element already in the set
|
let mut next_hs: HashSet<RecursionState> = HashSet::new();
|
||||||
// we've processed something better before already, and there is no point in continuing.
|
// The HashSet *of* the current timeslot that is being processed.
|
||||||
let res = self.pareto_frontier.update(rs);
|
let mut hs: HashSet<RecursionState>;
|
||||||
if res.is_lt() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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.
|
// Go through all five options and branch down them, if possible.
|
||||||
// Specifically, we can either:
|
// Specifically, we can either:
|
||||||
// -> Build one of the four robot types, if resources permit, and
|
// -> Build one of the four robot types, if resources permit.
|
||||||
// recurse, w/o letting any time pass.
|
// -> Don't build anything at all.
|
||||||
// -> 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,
|
// 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
|
// 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.
|
// 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
|
// (1) Ore Robot
|
||||||
if rs.ore >= self.ore_robot_ore_cost {
|
if rs.ore >= self.ore_robot_ore_cost {
|
||||||
// Commission the robot, subtracting the resources.
|
let mut nrs = *rs;
|
||||||
rs.ore -= self.ore_robot_ore_cost;
|
nrs.ore -= self.ore_robot_ore_cost;
|
||||||
// Let time pass.
|
nrs.pass_time();
|
||||||
rs.pass_time();
|
nrs.ore_robots += 1;
|
||||||
// Add the commissioned robot and recurse.
|
next_hs.insert(nrs);
|
||||||
rs.ore_robots += 1;
|
insert_count += 1;
|
||||||
self.solve(rs);
|
// checked_insert(&mut q, nrs);
|
||||||
// "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
|
// (2) Clay Robot
|
||||||
if rs.ore >= self.clay_robot_ore_cost {
|
if rs.ore >= self.clay_robot_ore_cost {
|
||||||
// Commission the robot, subtracting the resources.
|
let mut nrs = *rs;
|
||||||
rs.ore -= self.clay_robot_ore_cost;
|
nrs.ore -= self.clay_robot_ore_cost;
|
||||||
// Let time pass.
|
nrs.pass_time();
|
||||||
rs.pass_time();
|
nrs.clay_robots += 1;
|
||||||
// Add the commissioned robot and recurse.
|
next_hs.insert(nrs);
|
||||||
rs.clay_robots += 1;
|
insert_count += 1;
|
||||||
self.solve(rs);
|
// checked_insert(&mut q, nrs);
|
||||||
// "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
|
// (3) Obsidian Robot
|
||||||
if rs.ore >= self.obsidian_robot_ore_cost && rs.clay >= self.obsidian_robot_clay_cost {
|
if rs.ore >= self.obsidian_robot_ore_cost
|
||||||
// Commission the robot, subtracting the resources.
|
&& rs.clay >= self.obsidian_robot_clay_cost
|
||||||
rs.ore -= self.obsidian_robot_ore_cost;
|
{
|
||||||
rs.clay -= self.obsidian_robot_clay_cost;
|
let mut nrs = *rs;
|
||||||
// Let time pass.
|
nrs.ore -= self.obsidian_robot_ore_cost;
|
||||||
rs.pass_time();
|
nrs.clay -= self.obsidian_robot_clay_cost;
|
||||||
// Add the commissioned robot and recurse.
|
nrs.pass_time();
|
||||||
rs.obsidian_robots += 1;
|
nrs.obsidian_robots += 1;
|
||||||
self.solve(rs);
|
next_hs.insert(nrs);
|
||||||
// "Unbuild" the robot in order to check the other possible timelines.
|
insert_count += 1;
|
||||||
// First remove the robot, then "unpass" the time, and then re-add the resources.
|
// checked_insert(&mut q, nrs);
|
||||||
rs.obsidian_robots -= 1;
|
|
||||||
rs.reverse_time();
|
|
||||||
rs.ore += self.obsidian_robot_ore_cost;
|
|
||||||
rs.clay += self.obsidian_robot_clay_cost;
|
|
||||||
}
|
}
|
||||||
// (4) Geode Robot
|
// (4) Geode Robot
|
||||||
if rs.ore >= self.geode_robot_ore_cost && rs.obsidian >= self.geode_robot_obsidian_cost {
|
if rs.ore >= self.geode_robot_ore_cost
|
||||||
// Commission the robot, subtracting the resources.
|
&& rs.obsidian >= self.geode_robot_obsidian_cost
|
||||||
rs.ore -= self.geode_robot_ore_cost;
|
{
|
||||||
rs.obsidian -= self.geode_robot_obsidian_cost;
|
let mut nrs = *rs;
|
||||||
// Let time pass.
|
nrs.ore -= self.geode_robot_ore_cost;
|
||||||
rs.pass_time();
|
nrs.obsidian -= self.geode_robot_obsidian_cost;
|
||||||
// Add the commissioned robot and recurse.
|
nrs.pass_time();
|
||||||
rs.geode_robots += 1;
|
nrs.geode_robots += 1;
|
||||||
self.solve(rs);
|
next_hs.insert(nrs);
|
||||||
// "Unbuild" the robot in order to check the other possible timelines.
|
insert_count += 1;
|
||||||
// First remove the robot, then "unpass" the time, and then re-add the resources.
|
// checked_insert(&mut q, nrs);
|
||||||
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.
|
// (5) Build nothing and let time pass.
|
||||||
rs.pass_time();
|
let mut nrs = *rs;
|
||||||
self.solve(rs);
|
nrs.pass_time();
|
||||||
rs.reverse_time();
|
next_hs.insert(nrs);
|
||||||
|
insert_count += 1;
|
||||||
|
// checked_insert(&mut q, nrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
println!("Found optimal geode count: {}", self.optimal_geode_count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,17 +182,6 @@ impl RecursionState {
|
|||||||
self.remaining_time -= 1;
|
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.
|
// Compare two recursion states.
|
||||||
// This method essentially checks for Pareto improvements. An example:
|
// This method essentially checks for Pareto improvements. An example:
|
||||||
// RS1: 2 ore, 2 clay, 1 ore robot, 1 clay robot, 20 minutes left
|
// 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.
|
// This comparison is used to cut off redundant simulation paths in the recursive solver.
|
||||||
fn compare(&self, other: &Self) -> Ordering {
|
fn compare(&self, other: &Self) -> Ordering {
|
||||||
// Collect the comparison result on all metrics.
|
// 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
|
// Resource counts
|
||||||
metrics.push(self.ore.cmp(&other.ore));
|
metrics[0] = self.ore.cmp(&other.ore);
|
||||||
metrics.push(self.clay.cmp(&other.clay));
|
metrics[1] = self.clay.cmp(&other.clay);
|
||||||
metrics.push(self.obsidian.cmp(&other.obsidian));
|
metrics[2] = self.obsidian.cmp(&other.obsidian);
|
||||||
metrics.push(self.geode.cmp(&other.geode));
|
metrics[3] = self.geode.cmp(&other.geode);
|
||||||
// Robot counts
|
// Robot counts
|
||||||
metrics.push(self.ore_robots.cmp(&other.ore_robots));
|
metrics[4] = self.ore_robots.cmp(&other.ore_robots);
|
||||||
metrics.push(self.clay_robots.cmp(&other.clay_robots));
|
metrics[5] = self.clay_robots.cmp(&other.clay_robots);
|
||||||
metrics.push(self.obsidian_robots.cmp(&other.obsidian_robots));
|
metrics[6] = self.obsidian_robots.cmp(&other.obsidian_robots);
|
||||||
metrics.push(self.geode_robots.cmp(&other.geode_robots));
|
metrics[7] = self.geode_robots.cmp(&other.geode_robots);
|
||||||
// Remaining time
|
// 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,
|
// If one metric is strictly superior and the rest are better or equal,
|
||||||
// this RecursionState is "strictly better".
|
// 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() {
|
fn main() {
|
||||||
// Use command line arguments to specify the input filename.
|
// Use command line arguments to specify the input filename.
|
||||||
let args: Vec<String> = std::env::args().collect();
|
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_ore_cost: l[27].parse::<usize>().unwrap(),
|
||||||
geode_robot_obsidian_cost: l[30].parse::<usize>().unwrap(),
|
geode_robot_obsidian_cost: l[30].parse::<usize>().unwrap(),
|
||||||
optimal_geode_count: 0,
|
optimal_geode_count: 0,
|
||||||
// Start with an empty ParetoFrontier.
|
|
||||||
pareto_frontier: ParetoFrontier::new(),
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.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.
|
// Solve for every blueprint.
|
||||||
for bp in &mut blueprints {
|
for bp in &mut blueprints {
|
||||||
// Solve every blueprint with 24 minutes of time.
|
// Solve every blueprint with 24 minutes of time.
|
||||||
println!("Solving Blueprint {}", bp.id);
|
println!("Solving Blueprint {}", bp.id);
|
||||||
bp.solve(&mut rs);
|
bp.solve();
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user