First version for 2022/day19-part1, not yet finished
This commit is contained in:
parent
17b38905fb
commit
674ab048a3
8
2022/day19-part1/Cargo.toml
Normal file
8
2022/day19-part1/Cargo.toml
Normal 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]
|
||||
299
2022/day19-part1/src/main.rs
Normal file
299
2022/day19-part1/src/main.rs
Normal 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>()
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user