diff --git a/data/day19/example01.txt b/data/day19/example01.txt new file mode 100644 index 0000000..6f9d45b --- /dev/null +++ b/data/day19/example01.txt @@ -0,0 +1,11 @@ +Blueprint 1: + Each ore robot costs 4 ore. + Each clay robot costs 2 ore. + Each obsidian robot costs 3 ore and 14 clay. + Each geode robot costs 2 ore and 7 obsidian. + +Blueprint 2: + Each ore robot costs 2 ore. + Each clay robot costs 3 ore. + Each obsidian robot costs 3 ore and 8 clay. + Each geode robot costs 3 ore and 12 obsidian. \ No newline at end of file diff --git a/data/day19/input.txt b/data/day19/input.txt new file mode 100644 index 0000000..f8f652c --- /dev/null +++ b/data/day19/input.txt @@ -0,0 +1,30 @@ +Blueprint 1: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 14 clay. Each geode robot costs 4 ore and 11 obsidian. +Blueprint 2: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 20 clay. Each geode robot costs 2 ore and 12 obsidian. +Blueprint 3: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 20 clay. Each geode robot costs 2 ore and 10 obsidian. +Blueprint 4: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 4 ore and 16 clay. Each geode robot costs 2 ore and 15 obsidian. +Blueprint 5: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 7 clay. Each geode robot costs 4 ore and 20 obsidian. +Blueprint 6: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 19 clay. Each geode robot costs 2 ore and 18 obsidian. +Blueprint 7: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 14 clay. Each geode robot costs 3 ore and 20 obsidian. +Blueprint 8: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 16 clay. Each geode robot costs 4 ore and 17 obsidian. +Blueprint 9: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 11 clay. Each geode robot costs 4 ore and 7 obsidian. +Blueprint 10: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 17 clay. Each geode robot costs 3 ore and 16 obsidian. +Blueprint 11: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 19 clay. Each geode robot costs 4 ore and 12 obsidian. +Blueprint 12: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 16 clay. Each geode robot costs 2 ore and 18 obsidian. +Blueprint 13: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 13 clay. Each geode robot costs 2 ore and 20 obsidian. +Blueprint 14: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 16 clay. Each geode robot costs 3 ore and 14 obsidian. +Blueprint 15: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 17 clay. Each geode robot costs 4 ore and 16 obsidian. +Blueprint 16: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 8 clay. Each geode robot costs 3 ore and 19 obsidian. +Blueprint 17: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 19 clay. Each geode robot costs 3 ore and 19 obsidian. +Blueprint 18: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 11 clay. Each geode robot costs 2 ore and 8 obsidian. +Blueprint 19: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 7 clay. Each geode robot costs 3 ore and 10 obsidian. +Blueprint 20: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 18 clay. Each geode robot costs 4 ore and 12 obsidian. +Blueprint 21: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 19 clay. Each geode robot costs 3 ore and 17 obsidian. +Blueprint 22: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 20 clay. Each geode robot costs 2 ore and 12 obsidian. +Blueprint 23: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 7 clay. Each geode robot costs 3 ore and 9 obsidian. +Blueprint 24: Each ore robot costs 2 ore. Each clay robot costs 2 ore. Each obsidian robot costs 2 ore and 17 clay. Each geode robot costs 2 ore and 10 obsidian. +Blueprint 25: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 15 clay. Each geode robot costs 2 ore and 8 obsidian. +Blueprint 26: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 14 clay. Each geode robot costs 3 ore and 17 obsidian. +Blueprint 27: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 19 clay. Each geode robot costs 3 ore and 8 obsidian. +Blueprint 28: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 8 clay. Each geode robot costs 2 ore and 12 obsidian. +Blueprint 29: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 19 clay. Each geode robot costs 3 ore and 13 obsidian. +Blueprint 30: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 6 clay. Each geode robot costs 4 ore and 11 obsidian. \ No newline at end of file diff --git a/src/days/day19/mod.rs b/src/days/day19/mod.rs new file mode 100644 index 0000000..32bf5a4 --- /dev/null +++ b/src/days/day19/mod.rs @@ -0,0 +1,587 @@ +use super::template::{DayTrait, ResultType}; +use crate::common::parser::{extract_result, ignore, trim0, trim1, trim_left1}; +use itertools::Itertools; +use nom::{ + branch::alt, + bytes::complete::tag, + character::complete::{char, i64, multispace0, u32}, + combinator::{value, verify}, + error::Error, + multi::{count, many0, separated_list1}, + sequence::{preceded, tuple}, + Err, IResult, Parser, +}; +use std::{ + collections::{BinaryHeap, HashMap}, + mem, + ops::{Add, IndexMut, Sub}, + sync::mpsc, + thread, +}; +use std::{ops::Index, str::FromStr}; +use thiserror::Error; + +const DAY_NUMBER: usize = 19; + +const NUMBER_MATERIALS: usize = 4; +const USE_THREADED: bool = true; + +pub struct Day; + +impl DayTrait for Day { + fn get_day_number(&self) -> usize { + DAY_NUMBER + } + + fn part1(&self, lines: &str) -> anyhow::Result { + let cabinet: Cabinet = lines.parse()?; + let result = if USE_THREADED { + cabinet.threaded_quality_level(24)? + } else { + cabinet.quality_level(24)? + }; + Ok(ResultType::Integer(result)) + } + + fn part2(&self, lines: &str) -> anyhow::Result { + let cabinet: Cabinet = lines.parse()?; + let result = if USE_THREADED { + cabinet.threaded_reduced_quality(3, 32)? + } else { + cabinet.reduced_quality(3, 32)? + }; + Ok(ResultType::Integer(result)) + } +} + +#[derive(Debug, Error)] +enum RobotError { + #[error("Not a valid description: {0}")] + ParsingError(String), + + #[error("No optimum was found")] + NoOptimumFound, +} + +impl From>> for RobotError { + fn from(error: Err>) -> Self { + RobotError::ParsingError(error.to_string()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Hash)] +enum Material { + Geode = 0, + Obsidian = 1, + Clay = 2, + Ore = 3, +} + +impl Material { + pub fn prev(&self) -> Option { + match self { + Material::Geode => None, + Material::Obsidian => Some(Material::Geode), + Material::Clay => Some(Material::Obsidian), + Material::Ore => Some(Material::Clay), + } + } + + pub fn next(&self) -> Option { + match self { + Material::Geode => Some(Material::Obsidian), + Material::Obsidian => Some(Material::Clay), + Material::Clay => Some(Material::Ore), + Material::Ore => None, + } + } + + pub fn parse(input: &str) -> IResult<&str, Material> { + alt(( + value(Material::Ore, tag("ore")), + value(Material::Clay, tag("clay")), + value(Material::Obsidian, tag("obsidian")), + value(Material::Geode, tag("geode")), + ))(input) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct Ingredients([i64; NUMBER_MATERIALS]); + +impl Ingredients { + pub fn new(items: Vec<(i64, Material)>) -> Self { + let mut ingredients = [0; NUMBER_MATERIALS]; + for (amount, material) in items { + ingredients[material as usize] = amount; + } + Ingredients(ingredients) + } + + pub fn is_non_negative(&self) -> bool { + self.0.iter().all(|item| *item >= 0) + } + + fn inc(&self, mat: Material) -> Ingredients { + let mut next = self.0.clone(); + next[mat as usize] += 1; + Ingredients(next) + } + + fn pos_max(&self, other: &Ingredients) -> Self { + let next = self + .0 + .iter() + .zip(other.0.iter()) + .map(|(a, b)| *a.max(b)) + .collect_vec() + .try_into() + .unwrap(); + Ingredients(next) + } + + fn none_smaller(&self, other: &Ingredients) -> bool { + self.0.iter().zip(other.0.iter()).all(|(a, b)| a >= b) + } +} + +impl Index for Ingredients { + type Output = i64; + + fn index(&self, index: Material) -> &Self::Output { + &self.0[index as usize] + } +} + +impl IndexMut for Ingredients { + fn index_mut(&mut self, index: Material) -> &mut Self::Output { + &mut self.0[index as usize] + } +} + +impl Add<&Ingredients> for Ingredients { + type Output = Ingredients; + + fn add(self, rhs: &Ingredients) -> Self::Output { + let mut result = self.clone(); + for pos in 0..NUMBER_MATERIALS { + result.0[pos] += rhs.0[pos]; + } + result + } +} + +impl Add<&Ingredients> for &Ingredients { + type Output = Ingredients; + + fn add(self, rhs: &Ingredients) -> Self::Output { + let mut result = self.clone(); + for pos in 0..NUMBER_MATERIALS { + result.0[pos] += rhs.0[pos]; + } + result + } +} + +impl Sub<&Ingredients> for Ingredients { + type Output = Ingredients; + + fn sub(self, rhs: &Self) -> Self::Output { + let mut result = self.clone(); + for pos in 0..NUMBER_MATERIALS { + result.0[pos] -= rhs.0[pos]; + } + result + } +} + +impl Sub<&Ingredients> for &Ingredients { + type Output = Ingredients; + + fn sub(self, rhs: &Ingredients) -> Self::Output { + let mut result = self.clone(); + for pos in 0..NUMBER_MATERIALS { + result.0[pos] -= rhs.0[pos]; + } + result + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +struct Blueprint { + id: usize, + material: [Ingredients; NUMBER_MATERIALS], + max_robots: Ingredients, +} + +impl Blueprint { + pub fn new(id: usize, material: [Ingredients; NUMBER_MATERIALS]) -> Self { + let mut max_robots = material[Material::Geode as usize] + .pos_max(&material[Material::Obsidian as usize]) + .pos_max(&material[Material::Clay as usize]); + max_robots[Material::Geode] = i32::MAX as i64; + + Blueprint { + id, + material, + max_robots, + } + } + + pub fn ingredients_for(&self, mat: Material) -> &Ingredients { + &self.material[mat as usize] + } + + fn parse_line(input: &str) -> IResult<&str, (Material, Ingredients)> { + let input = ignore(tag("Each"))(input)?; + let (input, robot) = trim1(Material::parse)(input)?; + let input = ignore(tag("robot costs"))(input)?; + let (input, ingredients) = + separated_list1(tag("and"), tuple((trim0(i64), trim0(Material::parse))))(input)?; + let input = ignore(char('.'))(input)?; + Ok((input, (robot, Ingredients::new(ingredients)))) + } + + fn parse(start: &str) -> IResult<&str, Self> { + let input = ignore(tag("Blueprint"))(start)?; + let (input, id) = trim_left1(u32.map(|v| v as usize))(input)?; + let input = ignore(char(':'))(input)?; + let (input, robots) = verify( + count( + preceded(multispace0, Blueprint::parse_line), + NUMBER_MATERIALS, + ), + |robots: &[(Material, Ingredients)]| { + robots.iter().map(|(material, _)| material).all_unique() + }, + )(input)?; + + let material: [Ingredients; NUMBER_MATERIALS] = robots + .into_iter() + .sorted_by_key(|(material, _)| *material) + .map(|(_, ingreditents)| ingreditents) + .collect_vec() + .try_into() + .unwrap(); + + Ok((input, Blueprint::new(id, material))) + } + + pub fn simulate(&self, max_time: usize) -> Result<(usize, i64), RobotError> { + let simulation = Simulation::new(max_time); + let mut queue = BinaryHeap::new(); + let mut seen: HashMap<(Ingredients, usize), Ingredients> = HashMap::new(); + queue.push(simulation); + + while let Some(current) = queue.pop() { + if current.time == 0 { + return Ok((self.id, current.material(Material::Geode))); + } + let key = (current.all_robots().clone(), current.time); + if let Some(last) = seen.get(&key) { + if last.none_smaller(current.all_material()) { + continue; + } + } + seen.insert(key, current.all_material().clone()); + + queue.extend(current.next_round(self)); + } + + Err(RobotError::NoOptimumFound) + } +} + +impl FromStr for Blueprint { + type Err = RobotError; + + fn from_str(input: &str) -> Result { + Ok(extract_result(Blueprint::parse)(input)?) + } +} + +struct Cabinet(Vec); + +impl FromStr for Cabinet { + type Err = RobotError; + + fn from_str(line: &str) -> Result { + let blueprints = extract_result(many0(preceded(multispace0, Blueprint::parse)))(line)?; + Ok(Cabinet(blueprints)) + } +} + +impl Cabinet { + pub fn quality_level(&self, max_time: usize) -> Result { + self.0 + .iter() + .map(|blueprint| blueprint.simulate(max_time)) + .map_ok(|(id, geodes)| id as i64 * geodes) + .fold_ok(0, |a, b| a + b) + } + + pub fn reduced_quality(&self, count: usize, max_time: usize) -> Result { + self.0 + .iter() + .take(count) + .map(|blueprint| blueprint.simulate(max_time)) + .map_ok(|(_, geodes)| geodes) + .fold_ok(1, |a, b| a * b) + } + + pub fn threaded_quality_level(self, max_time: usize) -> Result { + let (sender, receiver) = mpsc::channel(); + for blueprint in self.0 { + let sender = sender.clone(); + thread::spawn(move || { + let result = blueprint.simulate(max_time); + let _ = sender.send(result); + }); + } + mem::drop(sender); + + receiver + .into_iter() + .map_ok(|(id, geodes)| id as i64 * geodes) + .fold_ok(0, |a, b| a + b) + } + + pub fn threaded_reduced_quality( + self, + count: usize, + max_time: usize, + ) -> Result { + let (sender, receiver) = mpsc::channel(); + let mut done = 0; + for blueprint in self.0 { + let sender = sender.clone(); + thread::spawn(move || { + let result = blueprint.simulate(max_time); + let _ = sender.send(result); + }); + + done = done + 1; + if done >= count { + break; + } + } + mem::drop(sender); + + receiver + .into_iter() + .map_ok(|(_, geodes)| geodes) + .fold_ok(1, |a, b| a * b) + } +} + +#[derive(Debug, Clone)] +struct Simulation { + time: usize, + robots: Ingredients, + material: Ingredients, +} + +impl Simulation { + pub fn new(time: usize) -> Self { + let _robots = Ingredients::new(vec![(1, Material::Ore)]); + let _material = Ingredients::new(vec![]); + Self { + time, + robots: _robots, + material: _material, + } + } + + #[inline] + pub fn all_robots(&self) -> &Ingredients { + &self.robots + } + + #[inline] + pub fn robots_for(&self, mat: Material) -> i64 { + self.robots[mat] + } + + #[inline] + pub fn all_material(&self) -> &Ingredients { + &self.material + } + + #[inline] + pub fn material(&self, mat: Material) -> i64 { + self.material[mat] + } + + pub fn no_production(mut self) -> Self { + self.time = self.time - 1; + self.material = self.material + &self.robots; + self + } + + fn check_create(&self, blueprint: &Blueprint, mat: Material) -> Option { + let rest = self.all_material() - blueprint.ingredients_for(mat); + if rest.is_non_negative() { + let robots = self.all_robots().inc(mat); + let material = rest + self.all_robots(); + Some(Simulation { + time: self.time - 1, + robots, + material, + }) + } else { + None + } + } + + pub fn next_round(self, blueprint: &Blueprint) -> Vec { + let mut result = vec![]; + if let Some(next) = self.check_create(blueprint, Material::Geode) { + result.push(next); + } + + let mut current = Some(Material::Obsidian); + while let Some(mat) = current { + if self.robots_for(mat) < blueprint.max_robots[mat] + && self.material(mat) <= 4 * blueprint.max_robots[mat] / 3 + { + if let Some(next) = self.check_create(blueprint, mat) { + result.push(next); + } + } + current = mat.next(); + } + if result.len() <= 1 { + result.push(self.no_production()); + } + result + } +} + +impl PartialEq for Simulation { + fn eq(&self, other: &Self) -> bool { + self.time == other.time && self.material == other.material + } +} + +impl Eq for Simulation {} + +impl PartialOrd for Simulation { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Simulation { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self.time.cmp(&other.time) { + core::cmp::Ordering::Equal => {} + ord => return ord, + } + self.material.cmp(&other.material) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::common::file::read_string; + use anyhow::Result; + + #[test] + fn test_part1() -> Result<()> { + let day = Day {}; + let lines = read_string(day.get_day_number(), "example01.txt")?; + let expected = ResultType::Integer(33); + let result = day.part1(&lines)?; + assert_eq!(result, expected); + + Ok(()) + } + + #[test] + fn test_part2() -> Result<()> { + let day = Day {}; + let lines = read_string(day.get_day_number(), "example01.txt")?; + let expected = ResultType::Integer(56 * 62); + let result = day.part2(&lines)?; + assert_eq!(result, expected); + + Ok(()) + } + + #[test] + fn parse() -> Result<()> { + let line = "Blueprint 1: Each ore robot costs 4 ore. Each clay robot costs 2 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 2 ore and 7 obsidian."; + let expected = Blueprint::new( + 1, + [ + Ingredients([0, 7, 0, 2]), + Ingredients([0, 0, 14, 3]), + Ingredients([0, 0, 0, 2]), + Ingredients([0, 0, 0, 4]), + ], + ); + let result: Blueprint = line.parse()?; + assert_eq!(result, expected); + + Ok(()) + } + + #[test] + fn parse_faulty() { + let line = "Blueprint 1: Each ore robot costs 4 ore. Each ore robot costs 2 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 2 ore and 7 obsidian."; + let result: Result = line.parse(); + assert!(result.is_err()); + } + + #[test] + fn parse_many() -> Result<()> { + let day = Day {}; + let lines = read_string(day.get_day_number(), "example01.txt")?; + let cabinet: Cabinet = lines.parse()?; + assert_eq!(cabinet.0.len(), 2); + + Ok(()) + } + + #[test] + fn simulate_one() -> Result<()> { + let line = "Blueprint 1: Each ore robot costs 4 ore. Each clay robot costs 2 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 2 ore and 7 obsidian."; + let blueprint: Blueprint = line.parse()?; + let result = blueprint.simulate(24)?; + assert_eq!(result, (1, 9)); + + Ok(()) + } + + #[test] + fn simulate_two() -> Result<()> { + let line = "Blueprint 2: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 8 clay. Each geode robot costs 3 ore and 12 obsidian."; + let blueprint: Blueprint = line.parse()?; + let result = blueprint.simulate(24)?; + assert_eq!(result, (2, 12)); + + Ok(()) + } + + #[test] + fn simulate_one_two() -> Result<()> { + let line = "Blueprint 1: Each ore robot costs 4 ore. Each clay robot costs 2 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 2 ore and 7 obsidian."; + let blueprint: Blueprint = line.parse()?; + let result = blueprint.simulate(32)?; + assert_eq!(result, (1, 56)); + + Ok(()) + } + + #[test] + fn simulate_two_two() -> Result<()> { + let line = "Blueprint 2: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 8 clay. Each geode robot costs 3 ore and 12 obsidian."; + + let blueprint: Blueprint = line.parse()?; + let result = blueprint.simulate(32)?; + assert_eq!(result, (2, 62)); + + Ok(()) + } +} diff --git a/src/days/mod.rs b/src/days/mod.rs index d74c58b..80b70ea 100644 --- a/src/days/mod.rs +++ b/src/days/mod.rs @@ -16,6 +16,7 @@ mod day15; mod day16; mod day17; mod day18; +mod day19; mod template; pub use template::DayTrait; @@ -25,7 +26,7 @@ pub mod day_provider { use super::*; use thiserror::Error; - const MAX_DAY: usize = 18; + const MAX_DAY: usize = 19; pub fn get_day(day_num: usize) -> Result, ProviderError> { match day_num { @@ -47,6 +48,7 @@ pub mod day_provider { 16 => Ok(Box::new(day16::Day)), 17 => Ok(Box::new(day17::Day)), 18 => Ok(Box::new(day18::Day)), + 19 => Ok(Box::new(day19::Day)), _ => Err(ProviderError::InvalidNumber(day_num)), } }