From 33b88bbd414efe365c018bc1f72bbf83606be9cd Mon Sep 17 00:00:00 2001 From: Cass Fridkin Date: Wed, 25 Sep 2024 14:54:11 -0600 Subject: [PATCH] Solve day 15 :) --- .github/workflows/test.yml | 2 + src/2015/15.rs | 179 ++++++++++++++++++++++++++++++------- tests/2015.rs | 4 +- 3 files changed, 151 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce11028..5ac96d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,3 +40,5 @@ jobs: INPUT_2015_11: ${{ secrets.INPUT_2015_11 }} INPUT_2015_12: ${{ secrets.INPUT_2015_12 }} INPUT_2015_13: ${{ secrets.INPUT_2015_13 }} + INPUT_2015_14: ${{ secrets.INPUT_2015_14 }} + INPUT_2015_15: ${{ secrets.INPUT_2015_15 }} diff --git a/src/2015/15.rs b/src/2015/15.rs index 1f0bb71..dcca9fc 100644 --- a/src/2015/15.rs +++ b/src/2015/15.rs @@ -1,8 +1,8 @@ use std::{ collections::{HashMap, HashSet}, + fmt::{self, Display, Formatter}, hash::{Hash, Hasher}, ops::{Add, AddAssign}, - sync::OnceLock, }; use eyre::{eyre, OptionExt, Report, Result}; @@ -18,17 +18,20 @@ use winnow::{ use crate::types::{problem, Problem}; -pub const SCIENCE_FOR_HUNGRY_PEOPLE: Problem = problem!(best_cookie); +pub const SCIENCE_FOR_HUNGRY_PEOPLE: Problem = problem!( + |input| best_cookie(input, None), + |input| best_cookie(input, Some(500)) +); +const NUM_TABLESPOONS: usize = 100; -fn best_cookie(input: &str) -> Result { - let kitchen = input - .lines() - .map(|line| line.try_into()) - .collect::>()?; - let cookie = kitchen.bake()?; +fn best_cookie(input: &str, calorie_restriction: Option) -> Result { + let kitchen = Kitchen::from_input(input)?; + + let cookie = kitchen.best_cookie(calorie_restriction)?; Ok(cookie.score()) } +#[derive(Debug, PartialEq)] struct Kitchen<'s> { ingredients: HashSet, FnvBuildHasher>, } @@ -42,51 +45,76 @@ impl<'s> FromIterator> for Kitchen<'s> { } impl<'s> Kitchen<'s> { - fn bake(&self) -> Result> { - // bake `self.ingredients.len() choose 100 with replacement` cookies + fn from_input(input: &'s str) -> Result { + input.lines().map(|line| line.trim().try_into()).collect() + } + + fn best_cookie(&self, calorie_restriction: Option) -> Result> { self.ingredients .iter() .copied() - .combinations_with_replacement(100) + .combinations_with_replacement(NUM_TABLESPOONS) .par_bridge() - .map(|tablespoons| { - debug_assert_eq!(tablespoons.len(), 100); - tablespoons.into_iter().collect() + .map(Cookie::bake) + .filter(|cookie| { + if let Some(cal_limit) = calorie_restriction { + cookie.calories() == cal_limit + } else { + true + } }) - .max_by_key(|cookie: &Cookie| cookie.score()) - .ok_or_eyre("baked with 0 ingredients") + .max_by_key(Cookie::score) + .ok_or_eyre("didn't bake any cookies") } } +#[derive(Debug)] struct Cookie<'s> { ingredients: HashMap, u8, FnvBuildHasher>, - score: OnceLock, + qualities: Qualities, +} + +impl Display for Cookie<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.ingredients + .iter() + .try_for_each(|(Ingredient { name, .. }, n)| writeln!(f, "- {n} tbsp {name}")) + } +} + +impl<'s> Cookie<'s> { + fn bake>>(ingredients: I) -> Self { + Self::from_iter(ingredients) + } } impl<'s> FromIterator> for Cookie<'s> { fn from_iter>>(iter: T) -> Self { - let mut map: HashMap, u8, FnvBuildHasher> = HashMap::default(); + let mut ingredients: HashMap, u8, FnvBuildHasher> = HashMap::default(); for ingredient in iter { - map.entry(ingredient).or_default().add_assign(1); + ingredients.entry(ingredient).or_default().add_assign(1); } + let qualities = ingredients + .iter() + .map(|(ingredient, n)| ingredient.tbsp(*n)) + .reduce(|a, b| a + b) + .unwrap_or_default(); + Self { - ingredients: map, - score: OnceLock::new(), + ingredients, + qualities, } } } impl Cookie<'_> { fn score(&self) -> usize { - *self.score.get_or_init(|| { - self.ingredients - .iter() - .map(|(ingredient, n)| ingredient.tbsp(*n)) - .reduce(|a, b| a + b) - .map(|q| q.score()) - .unwrap_or_default() - }) + self.qualities.score() + } + + fn calories(&self) -> usize { + self.qualities.calories.try_into().unwrap_or_default() } } @@ -136,7 +164,7 @@ impl Hash for Ingredient<'_> { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Default)] struct Qualities { capacity: isize, durability: isize, @@ -147,8 +175,16 @@ struct Qualities { impl Qualities { fn score(&self) -> usize { - let sum = self.capacity + self.durability + self.flavor + self.texture; - sum.try_into().unwrap_or_default() + if [self.capacity, self.durability, self.flavor, self.texture] + .iter() + .any(|quality| (isize::MIN..0).contains(quality)) + { + 0 + } else { + (self.capacity * self.durability * self.flavor * self.texture) + .try_into() + .unwrap_or_default() + } } } @@ -196,3 +232,82 @@ impl<'s> TryFrom<&'s str> for Ingredient<'s> { .map_err(|e| eyre!("{e}")) } } + +#[test] +fn example() { + let butterscotch = Ingredient { + name: "Butterscotch", + qualities: Qualities { + capacity: -1, + durability: -2, + flavor: 6, + texture: 3, + calories: 8, + }, + }; + let cinnamon = Ingredient { + name: "Cinnamon", + qualities: Qualities { + capacity: 2, + durability: 3, + flavor: -2, + texture: -1, + calories: 3, + }, + }; + + let mut ingredients = HashSet::with_hasher(FnvBuildHasher::default()); + ingredients.insert(butterscotch); + ingredients.insert(cinnamon); + + let expected_kitchen = Kitchen { ingredients }; + let actual_kitchen = Kitchen::from_input( + "Butterscotch: capacity -1, durability -2, flavor 6, texture 3, calories 8 + Cinnamon: capacity 2, durability 3, flavor -2, texture -1, calories 3", + ) + .unwrap(); + + assert_eq!(actual_kitchen, expected_kitchen); + + assert_eq!( + butterscotch.tbsp(44), + Qualities { + #[allow(clippy::neg_multiply)] + capacity: 44 * -1, + durability: 44 * -2, + flavor: 44 * 6, + texture: 44 * 3, + calories: 44 * 8 + } + ); + + assert_eq!( + cinnamon.tbsp(56), + Qualities { + capacity: 56 * 2, + durability: 56 * 3, + flavor: 56 * -2, + #[allow(clippy::neg_multiply)] + texture: 56 * -1, + calories: 3 * 56 + } + ); + + let sum = butterscotch.tbsp(44) + cinnamon.tbsp(56); + assert_eq!( + sum, + Qualities { + capacity: 68, + durability: 80, + flavor: 152, + texture: 76, + calories: 520 + } + ); + + let expected_score = sum.score(); + assert_eq!(expected_score, 62842880); + + let actual_score = actual_kitchen.best_cookie(None).unwrap().score(); + assert_eq!(actual_score, expected_score); +} diff --git a/tests/2015.rs b/tests/2015.rs index 1ea7810..bf79deb 100644 --- a/tests/2015.rs +++ b/tests/2015.rs @@ -171,11 +171,11 @@ mod day14 { mod day15 { #[test] fn part1() { - crate::util::aoc!(2015/15-1: 2696); + crate::util::aoc!(2015/15-1: 222870); } #[test] fn part2() { - crate::util::aoc!(2015/15-2: 1084); + crate::util::aoc!(2015/15-2: 117936); } }