From 9892e3ebde304726903a1e5c358d05c2e343ea5e Mon Sep 17 00:00:00 2001 From: Justin Wernick Date: Tue, 19 Apr 2022 20:26:36 +0200 Subject: Refile for merging repos --- 2019/src/bin/day_14.rs | 221 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 2019/src/bin/day_14.rs (limited to '2019/src/bin/day_14.rs') diff --git a/2019/src/bin/day_14.rs b/2019/src/bin/day_14.rs new file mode 100644 index 0000000..0532f5f --- /dev/null +++ b/2019/src/bin/day_14.rs @@ -0,0 +1,221 @@ +use rpds::map::red_black_tree_map::RedBlackTreeMap; +use rpds::rbt_map; +use rpds::vector::Vector; +use std::fmt; +use std::io; +use std::io::prelude::*; +use std::iter; +use std::process; +use std::str::FromStr; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +#[structopt(name = "Day 14: Space Stoichiometry")] +/// Finds how much ore you need to produce one fuel. +/// +/// Recipes are passed in on stdin, one per line. +/// +/// See https://adventofcode.com/2019/day/14 for details. +struct Opt { + #[structopt(long = "available-ore")] + available_ore: Option, +} + +fn main() { + let stdin = io::stdin(); + let opt = Opt::from_args(); + + let recipes = stdin + .lock() + .lines() + .map(|x| exit_on_failed_assertion(x, "Error reading input")) + .map(|x| exit_on_failed_assertion(x.parse::(), "Input was not a valid recipe")) + .collect::>(); + + match opt.available_ore { + Some(ore) => println!("{}", max_fuel_production(ore, &recipes)), + None => println!("{}", Desires::new(1).min_ore_required(&recipes)), + } +} + +fn exit_on_failed_assertion(data: Result, message: &str) -> A { + match data { + Ok(data) => data, + Err(e) => { + eprintln!("{}: {}", message, e); + process::exit(1); + } + } +} + +fn max_fuel_production(ore_max: i64, recipes: &[Recipe]) -> i64 { + binary_search_max_fuel_production( + ore_max / Desires::new(1).min_ore_required(&recipes), + 2 * ore_max / Desires::new(1).min_ore_required(&recipes), + ore_max, + recipes, + ) +} + +fn binary_search_max_fuel_production( + fuel_min: i64, + fuel_max: i64, + ore_max: i64, + recipes: &[Recipe], +) -> i64 { + if fuel_max - fuel_min <= 1 { + fuel_min + } else if Desires::new((fuel_min + fuel_max) / 2).min_ore_required(recipes) <= ore_max { + binary_search_max_fuel_production((fuel_min + fuel_max) / 2, fuel_max, ore_max, recipes) + } else { + binary_search_max_fuel_production(fuel_min, (fuel_min + fuel_max) / 2, ore_max, recipes) + } +} + +#[derive(Debug, Clone)] +struct Recipe { + ingredients: Vector, + output: Chemical, +} + +#[derive(Default, Debug, Clone)] +struct Chemical { + name: String, + quantity: i64, +} + +impl FromStr for Recipe { + type Err = ParseErr; + fn from_str(s: &str) -> Result { + // 2 XSNKB, 15 ZVMCB, 3 KDFNZ => 2 RFLX + s.replace(" => ", "=") + .replace(", ", ",") + .split(|c| c == ',' || c == '=') + .map(|chem| chem.parse::()) + .collect::, ParseErr>>() + .map(|chemicals| Recipe { + ingredients: chemicals + .drop_last() + .expect("Assertion failed: line did not have any chemicals"), + output: chemicals + .last() + .cloned() + .expect("Assertion failed: line did not have any chemicals"), + }) + } +} + +impl Recipe { + fn required_scale(&self, desired_quantity: i64) -> i64 { + (desired_quantity + self.output.quantity - 1) / self.output.quantity + } +} + +impl FromStr for Chemical { + type Err = ParseErr; + fn from_str(s: &str) -> Result { + // 1 FUEL + match s.split(' ').collect::>()[..] { + [quantity, name] => quantity + .parse::() + .map_err(|_| ParseErr) + .map(|q| Chemical { + name: name.to_string(), + quantity: q, + }), + _ => Err(ParseErr), + } + } +} + +impl Chemical { + fn scale(&self, scale: i64) -> Chemical { + Chemical { + name: self.name.clone(), + quantity: self.quantity * scale, + } + } +} + +#[derive(Debug, Clone, Copy)] +struct ParseErr; + +impl fmt::Display for ParseErr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Error parsing input") + } +} + +impl std::error::Error for ParseErr {} + +#[derive(Debug, Clone)] +struct Desires { + chemicals: RedBlackTreeMap, +} + +impl Desires { + fn new(fuel: i64) -> Desires { + Desires { + chemicals: rbt_map!["FUEL".to_string() => fuel, "ORE".to_string() => 0], + } + } + + fn min_ore_required(&self, recipes: &[Recipe]) -> i64 { + iter::successors(Some(self.clone()), |prev| Some(prev.next(recipes))) + .find(|desires| desires.is_only_ore()) + .unwrap() + .chemicals + .get("ORE") + .cloned() + .unwrap() + } + + fn is_only_ore(&self) -> bool { + !self + .chemicals + .iter() + .any(|(name, quantity)| *quantity > 0 && name != "ORE") + } + + fn next(&self, recipes: &[Recipe]) -> Desires { + self.chemicals + .iter() + .find(|(name, quantity)| **quantity > 0 && *name != "ORE") + .map(|(mixing, quantity)| { + self.with_mixed_recipe( + recipes + .iter() + .find(|recipe| recipe.output.name == *mixing) + .expect("Required chemical without a recipe"), + *quantity, + ) + }) + .unwrap_or(self.clone()) + } + + fn with_mixed_recipe(&self, recipe: &Recipe, desired_quantity: i64) -> Desires { + recipe.ingredients.iter().fold( + self.with_chemical( + recipe + .output + .scale(-1 * recipe.required_scale(desired_quantity)), + ), + |acc, next_ingredient| { + acc.with_chemical(next_ingredient.scale(recipe.required_scale(desired_quantity))) + }, + ) + } + + fn with_chemical(&self, chemical: Chemical) -> Desires { + Desires { + chemicals: match self.chemicals.get(&chemical.name) { + Some(existing_quantity) => self + .chemicals + .insert(chemical.name.clone(), existing_quantity + chemical.quantity), + None => self + .chemicals + .insert(chemical.name.clone(), chemical.quantity), + }, + } + } +} -- cgit v1.2.3