diff --git a/inputs/day19.txt b/inputs/day19.txt new file mode 100644 index 0000000..242b6dd --- /dev/null +++ b/inputs/day19.txt @@ -0,0 +1,30 @@ +Blueprint 1: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 7 clay. Each geode robot costs 2 ore and 19 obsidian. +Blueprint 2: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 20 clay. Each geode robot costs 4 ore and 18 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 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 19 clay. Each geode robot costs 2 ore and 12 obsidian. +Blueprint 5: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 20 clay. Each geode robot costs 3 ore and 14 obsidian. +Blueprint 6: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 15 clay. Each geode robot costs 3 ore and 7 obsidian. +Blueprint 7: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 19 clay. Each geode robot costs 2 ore and 20 obsidian. +Blueprint 8: 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 9: Each ore robot costs 2 ore. Each clay robot costs 2 ore. Each obsidian robot costs 2 ore and 8 clay. Each geode robot costs 2 ore and 14 obsidian. +Blueprint 10: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 11 clay. Each geode robot costs 3 ore and 14 obsidian. +Blueprint 11: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 5 clay. Each geode robot costs 4 ore and 8 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 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 11 clay. Each geode robot costs 2 ore and 10 obsidian. +Blueprint 14: 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 15: 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 16: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 20 clay. Each geode robot costs 2 ore and 17 obsidian. +Blueprint 17: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 4 ore and 8 obsidian. +Blueprint 18: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 9 clay. Each geode robot costs 3 ore and 9 obsidian. +Blueprint 19: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 10 clay. Each geode robot costs 3 ore and 14 obsidian. +Blueprint 20: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 13 clay. Each geode robot costs 3 ore and 12 obsidian. +Blueprint 21: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 4 ore and 15 clay. Each geode robot costs 4 ore and 9 obsidian. +Blueprint 22: 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 23: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 4 ore and 19 clay. Each geode robot costs 4 ore and 12 obsidian. +Blueprint 24: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 15 clay. Each geode robot costs 3 ore and 8 obsidian. +Blueprint 25: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 11 clay. Each geode robot costs 2 ore and 16 obsidian. +Blueprint 26: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 17 clay. Each geode robot costs 3 ore and 7 obsidian. +Blueprint 27: 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 3 ore and 20 obsidian. +Blueprint 28: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 10 clay. Each geode robot costs 2 ore and 10 obsidian. +Blueprint 29: 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 2 ore and 13 obsidian. +Blueprint 30: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 4 ore and 20 clay. Each geode robot costs 4 ore and 8 obsidian. diff --git a/src/bin/day19_1.rs b/src/bin/day19_1.rs new file mode 100644 index 0000000..7fc2a9c --- /dev/null +++ b/src/bin/day19_1.rs @@ -0,0 +1,8 @@ +use std::fs; + +use aoc2022::day19::process_part_1; + +fn main() { + let file = fs::read_to_string("./inputs/day19.txt").unwrap(); + println!("{}", process_part_1(&file)); +} diff --git a/src/bin/day19_2.rs b/src/bin/day19_2.rs new file mode 100644 index 0000000..aac7bfb --- /dev/null +++ b/src/bin/day19_2.rs @@ -0,0 +1,8 @@ +use std::fs; + +use aoc2022::day19::process_part_2; + +fn main() { + let file = fs::read_to_string("./inputs/day19.txt").unwrap(); + println!("{}", process_part_2(&file)); +} diff --git a/src/day19.rs b/src/day19.rs new file mode 100644 index 0000000..0e90b8e --- /dev/null +++ b/src/day19.rs @@ -0,0 +1,214 @@ +use std::collections::HashSet; + +use nom::bytes::complete::tag; +use nom::character::complete; +use nom::character::complete::newline; +use nom::combinator::map; +use nom::multi::separated_list1; +use nom::sequence::{delimited, preceded, separated_pair, tuple}; +use nom::IResult; +use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; + +pub fn process_part_1(input: &str) -> usize { + let blueprints = parse_input(input).unwrap().1; + blueprints + .par_iter() + .map(|bp| bp.id * find_best_geodes(bp, 24)) + .sum() +} + +pub fn process_part_2(input: &str) -> usize { + let blueprints = parse_input(input).unwrap().1; + blueprints + .par_iter() + .take(3) + .map(|bp| find_best_geodes(bp, 32)) + .product() +} + +#[derive(Debug)] +struct Blueprint { + id: usize, + ore_bot_cost: usize, + clay_bot_cost: usize, + obsidian_bot_cost: (usize, usize), + geode_bot_cost: (usize, usize), +} + +impl Blueprint { + fn max_ore_cost(&self) -> usize { + [ + self.ore_bot_cost, + self.clay_bot_cost, + self.obsidian_bot_cost.0, + self.geode_bot_cost.0, + ] + .into_iter() + .max() + .unwrap() + } +} + +#[derive(Default, Copy, Clone, Debug, Hash, Eq, PartialEq)] +struct State { + time: usize, + + ore: usize, + clay: usize, + obsidian: usize, + geode: usize, + + ore_bots: usize, + clay_bots: usize, + obsidian_bots: usize, + geode_bots: usize, +} + +impl State { + fn upper_limit_geodes(&self) -> usize { + // assumes we can create a bot per every minute + let build_geode_cap = (self.time * (self.time + 1)) / 2; + + self.geode + build_geode_cap + self.geode_bots * self.time + } + + fn tick(&self) -> Self { + State { + ore: self.ore + self.ore_bots, + clay: self.clay + self.clay_bots, + obsidian: self.obsidian + self.obsidian_bots, + geode: self.geode + self.geode_bots, + time: self.time - 1, + ..*self + } + } +} + +fn find_best_geodes(bp: &Blueprint, time: usize) -> usize { + let mut visited = HashSet::new(); + let mut stack = vec![State { + ore_bots: 1, + time, + ..Default::default() + }]; + let max_ore_cost = bp.max_ore_cost(); + let mut max_geodes = 0; + + while let Some(state) = stack.pop() { + if state.time == 0 { + max_geodes = max_geodes.max(state.geode); + continue; + } + + // Prune states where its immediately obvious we won't beat the maximum + if state.upper_limit_geodes() < max_geodes { + continue; + } + + // Best case, we can build a geode bot: + if state.ore >= bp.geode_bot_cost.0 && state.obsidian >= bp.geode_bot_cost.1 { + let next = State { + geode_bots: state.geode_bots + 1, + ore: state.ore - bp.geode_bot_cost.0 + state.ore_bots, + obsidian: state.obsidian - bp.geode_bot_cost.1 + state.obsidian_bots, + ..state.tick() + }; + if visited.insert(next.clone()) { + stack.push(next); + } + } + + // Obsidian bots are nice too + if state.ore >= bp.obsidian_bot_cost.0 && state.clay >= bp.obsidian_bot_cost.1 { + let next = State { + obsidian_bots: state.obsidian_bots + 1, + ore: state.ore - bp.obsidian_bot_cost.0 + state.ore_bots, + clay: state.clay - bp.obsidian_bot_cost.1 + state.clay_bots, + ..state.tick() + }; + if visited.insert(next.clone()) { + stack.push(next); + } + } + + // Clay? + if state.ore >= bp.clay_bot_cost { + let next = State { + clay_bots: state.clay_bots + 1, + ore: state.ore - bp.clay_bot_cost + state.ore_bots, + ..state.tick() + }; + if visited.insert(next.clone()) { + stack.push(next); + } + } + + // Ore? But only if we don't make enough to make any new bot per round we want + if state.ore >= bp.ore_bot_cost && state.ore_bots < max_ore_cost { + let next = State { + ore_bots: state.ore_bots + 1, + ore: state.ore - bp.ore_bot_cost + state.ore_bots, + ..state.tick() + }; + if visited.insert(next.clone()) { + stack.push(next); + } + } + + // Final option, do nothing + let next = state.tick(); + if visited.insert(next.clone()) { + stack.push(next); + } + } + + max_geodes +} + +fn parse_input(input: &str) -> IResult<&str, Vec> { + separated_list1(newline, parse_blueprint)(input) +} + +fn parse_blueprint(input: &str) -> IResult<&str, Blueprint> { + map( + tuple(( + preceded(tag("Blueprint "), complete::u64), + preceded(tag(": Each ore robot costs "), complete::u64), + preceded(tag(" ore. Each clay robot costs "), complete::u64), + preceded( + tag(" ore. Each obsidian robot costs "), + separated_pair(complete::u64, tag(" ore and "), complete::u64), + ), + delimited( + tag(" clay. Each geode robot costs "), + separated_pair(complete::u64, tag(" ore and "), complete::u64), + tag(" obsidian."), + ), + )), + |(id, ore_bot, clay_bot, (ob_ore, ob_clay), (gb_ore, gb_obs))| Blueprint { + id: id as usize, + ore_bot_cost: ore_bot as usize, + clay_bot_cost: clay_bot as usize, + obsidian_bot_cost: (ob_ore as usize, ob_clay as usize), + geode_bot_cost: (gb_ore as usize, gb_obs as usize), + }, + )(input) +} + +#[cfg(test)] +mod tests { + use super::*; + + const INPUT: &str = "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."; + + #[test] + fn day1() { + assert_eq!(process_part_1(INPUT), 33); + } + + #[test] + fn day2() { + assert_eq!(process_part_2(INPUT), 3472); + } +} diff --git a/src/lib.rs b/src/lib.rs index 33fdcfd..503083f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,3 +19,4 @@ pub mod day15; pub mod day16; pub mod day17; pub mod day18; +pub mod day19;