From 674ab048a39c4a4871aa95418df52f9b00facf00 Mon Sep 17 00:00:00 2001 From: Tobias Marschner Date: Sun, 7 Apr 2024 14:31:41 +0200 Subject: [PATCH] First version for 2022/day19-part1, not yet finished --- 2022/day19-part1/Cargo.toml | 8 + 2022/day19-part1/src/main.rs | 299 +++++++++++++++++++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 2022/day19-part1/Cargo.toml create mode 100644 2022/day19-part1/src/main.rs diff --git a/2022/day19-part1/Cargo.toml b/2022/day19-part1/Cargo.toml new file mode 100644 index 0000000..116da57 --- /dev/null +++ b/2022/day19-part1/Cargo.toml @@ -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] diff --git a/2022/day19-part1/src/main.rs b/2022/day19-part1/src/main.rs new file mode 100644 index 0000000..78f3936 --- /dev/null +++ b/2022/day19-part1/src/main.rs @@ -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 = 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 = std::env::args().collect(); + if args.len() < 2 { + panic!("Usage: ./main \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::>()) + .collect::>() + .iter() + .map(|l| Blueprint { + id: l[1].trim_end_matches(':').parse::().unwrap(), + ore_robot_ore_cost: l[6].parse::().unwrap(), + clay_robot_ore_cost: l[12].parse::().unwrap(), + obsidian_robot_ore_cost: l[18].parse::().unwrap(), + obsidian_robot_clay_cost: l[21].parse::().unwrap(), + geode_robot_ore_cost: l[27].parse::().unwrap(), + geode_robot_obsidian_cost: l[30].parse::().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::>(); + + // 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::() + ); +}