First version for 2022/day19-part1, not yet finished

This commit is contained in:
Tobias Marschner 2024-04-07 14:31:41 +02:00
parent 17b38905fb
commit 674ab048a3
2 changed files with 307 additions and 0 deletions

View File

@ -0,0 +1,8 @@
[package]
name = "day19-part1"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -0,0 +1,299 @@
use std::cmp::Ordering;
#[derive(Debug)]
struct Blueprint {
// The id and costs parsed from the input.
id: usize,
ore_robot_ore_cost: usize,
clay_robot_ore_cost: usize,
obsidian_robot_ore_cost: usize,
obsidian_robot_clay_cost: usize,
geode_robot_ore_cost: usize,
geode_robot_obsidian_cost: usize,
// 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,
// For the sake of cutting out redundant simulation paths keep track of the
// "best" RecursionState for each timeslot.
// This way, when a RecursionState is "strictly inferior" to a simulation we ran in the past,
// we know we don't have to bother with this one and can simply return.
optimal_resursion_states: [RecursionState; 24],
}
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;
}
return;
}
// Compare the current RecursionState at this time with the best RecursionState
// we've previously run.
// If it's equal, keep going, should be worthwhile.
// If it's strictly superior, store it and definitely keep going.
// If it's strictly inferior, there's no point in continuing, return now.
match rs.compare(&self.optimal_resursion_states[24 - rs.remaining_time]) {
Ordering::Equal => (),
Ordering::Greater => {
self.optimal_resursion_states[24 - rs.remaining_time] = *rs;
println!("New optimal RS for time {}.", rs.remaining_time);
for rs in &self.optimal_resursion_states {
print!("RS[{:>2}]: ", 24 - rs.remaining_time);
rs.print();
}
}
Ordering::Less => {
return;
}
}
// 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();
}
}
// Store all of the state that's passed up and down the recursion in one struct.
#[derive(Debug, Copy, Clone)]
struct RecursionState {
// The currently active fleet of robots.
ore_robots: usize,
clay_robots: usize,
obsidian_robots: usize,
geode_robots: usize,
// Our resources.
ore: usize,
clay: usize,
obsidian: usize,
geode: usize,
// How much time is left in the simulation.
remaining_time: usize,
}
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;
self.clay += self.clay_robots;
self.obsidian += self.obsidian_robots;
self.geode += self.geode_robots;
// One unit of time passes.
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
// 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.
fn compare(&self, other: &Self) -> Ordering {
// Collect the comparison result on all metrics.
let mut metrics: Vec<Ordering> = Vec::with_capacity(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));
// 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));
// Remaining time
metrics.push(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".
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 {
Ordering::Equal
}
}
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);
}
}
fn main() {
// Use command line arguments to specify the input filename.
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
panic!("Usage: ./main <input-file>\nNo input file provided. Exiting.");
}
// Next, read the contents of the input file into a string for easier processing.
let input = std::fs::read_to_string(&args[1]).expect("Error opening file");
// First, parse all the blueprints.
let mut blueprints = input
.lines()
.map(|l| l.split_whitespace().collect::<Vec<_>>())
.collect::<Vec<_>>()
.iter()
.map(|l| Blueprint {
id: l[1].trim_end_matches(':').parse::<usize>().unwrap(),
ore_robot_ore_cost: l[6].parse::<usize>().unwrap(),
clay_robot_ore_cost: l[12].parse::<usize>().unwrap(),
obsidian_robot_ore_cost: l[18].parse::<usize>().unwrap(),
obsidian_robot_clay_cost: l[21].parse::<usize>().unwrap(),
geode_robot_ore_cost: l[27].parse::<usize>().unwrap(),
geode_robot_obsidian_cost: l[30].parse::<usize>().unwrap(),
optimal_geode_count: 0,
// Auto-fill the optimal RecrusionState-array with a dummy "terrible" RecursionState
// which will get overridden as the simulation progresses.
optimal_resursion_states: [RecursionState {
ore_robots: 0,
clay_robots: 0,
obsidian_robots: 0,
geode_robots: 0,
ore: 0,
clay: 0,
obsidian: 0,
geode: 0,
remaining_time: 0,
}; 24],
})
.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);
}
println!(
"Total Quality Level: {}",
blueprints
.iter()
.map(|b| b.id * b.optimal_geode_count)
.sum::<usize>()
);
}