day 19 finished

This commit is contained in:
Rüdiger Ludwig 2023-08-02 17:58:09 +02:00
parent b00835c25e
commit 062ede1df1
4 changed files with 631 additions and 1 deletions

11
data/day19/example01.txt Normal file
View file

@ -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.

30
data/day19/input.txt Normal file
View file

@ -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.

587
src/days/day19/mod.rs Normal file
View file

@ -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<ResultType> {
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<ResultType> {
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<Err<Error<&str>>> for RobotError {
fn from(error: Err<Error<&str>>) -> 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<Material> {
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<Material> {
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<Material> for Ingredients {
type Output = i64;
fn index(&self, index: Material) -> &Self::Output {
&self.0[index as usize]
}
}
impl IndexMut<Material> 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<Self, Self::Err> {
Ok(extract_result(Blueprint::parse)(input)?)
}
}
struct Cabinet(Vec<Blueprint>);
impl FromStr for Cabinet {
type Err = RobotError;
fn from_str(line: &str) -> Result<Self, Self::Err> {
let blueprints = extract_result(many0(preceded(multispace0, Blueprint::parse)))(line)?;
Ok(Cabinet(blueprints))
}
}
impl Cabinet {
pub fn quality_level(&self, max_time: usize) -> Result<i64, RobotError> {
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<i64, RobotError> {
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<i64, RobotError> {
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<i64, RobotError> {
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<Simulation> {
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<Simulation> {
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<std::cmp::Ordering> {
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<Blueprint, _> = 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(())
}
}

View file

@ -16,6 +16,7 @@ mod day15;
mod day16; mod day16;
mod day17; mod day17;
mod day18; mod day18;
mod day19;
mod template; mod template;
pub use template::DayTrait; pub use template::DayTrait;
@ -25,7 +26,7 @@ pub mod day_provider {
use super::*; use super::*;
use thiserror::Error; use thiserror::Error;
const MAX_DAY: usize = 18; const MAX_DAY: usize = 19;
pub fn get_day(day_num: usize) -> Result<Box<dyn DayTrait>, ProviderError> { pub fn get_day(day_num: usize) -> Result<Box<dyn DayTrait>, ProviderError> {
match day_num { match day_num {
@ -47,6 +48,7 @@ pub mod day_provider {
16 => Ok(Box::new(day16::Day)), 16 => Ok(Box::new(day16::Day)),
17 => Ok(Box::new(day17::Day)), 17 => Ok(Box::new(day17::Day)),
18 => Ok(Box::new(day18::Day)), 18 => Ok(Box::new(day18::Day)),
19 => Ok(Box::new(day19::Day)),
_ => Err(ProviderError::InvalidNumber(day_num)), _ => Err(ProviderError::InvalidNumber(day_num)),
} }
} }