diff --git a/2022/input/19/input b/2022/input/19/input new file mode 100644 index 0000000..8fc3d1e --- /dev/null +++ b/2022/input/19/input @@ -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 20 clay. Each geode robot costs 2 ore and 12 obsidian. +Blueprint 2: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 15 clay. Each geode robot costs 3 ore and 9 obsidian. +Blueprint 3: 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 4: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 20 clay. Each geode robot costs 2 ore and 20 obsidian. +Blueprint 5: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 20 clay. Each geode robot costs 4 ore and 16 obsidian. +Blueprint 6: 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 7: 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 2 ore and 18 obsidian. +Blueprint 8: 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 8 obsidian. +Blueprint 9: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 9 clay. Each geode robot costs 4 ore and 16 obsidian. +Blueprint 10: 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 11: 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 12: 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 3 ore and 8 obsidian. +Blueprint 13: 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 14: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 17 clay. Each geode robot costs 3 ore and 11 obsidian. +Blueprint 15: 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 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 16 obsidian. +Blueprint 17: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 10 clay. Each geode robot costs 2 ore and 14 obsidian. +Blueprint 18: 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 3 ore and 12 obsidian. +Blueprint 19: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 20 clay. Each geode robot costs 3 ore and 18 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 3 ore and 13 obsidian. +Blueprint 21: 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 4 ore and 18 obsidian. +Blueprint 22: 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 23: 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 11 obsidian. +Blueprint 24: 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 2 ore and 9 obsidian. +Blueprint 25: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 6 clay. Each geode robot costs 2 ore and 16 obsidian. +Blueprint 26: 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 27: 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 20 obsidian. +Blueprint 28: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 20 clay. Each geode robot costs 2 ore and 19 obsidian. +Blueprint 29: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 14 clay. Each geode robot costs 3 ore and 16 obsidian. +Blueprint 30: 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 16 obsidian. diff --git a/2022/input/19/test-1 b/2022/input/19/test-1 new file mode 100644 index 0000000..f39c094 --- /dev/null +++ b/2022/input/19/test-1 @@ -0,0 +1,2 @@ +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. diff --git a/2022/src/bin/day19.rs b/2022/src/bin/day19.rs new file mode 100644 index 0000000..c20cd56 --- /dev/null +++ b/2022/src/bin/day19.rs @@ -0,0 +1,312 @@ +#![feature(test)] +use std::{ops::{AddAssign, SubAssign}, str::FromStr}; + +use anyhow::Result; +use aoc::Solver; + +// -- Runners -- +fn main() -> Result<()> { + Day::solve() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn part1_test1() -> Result<()> { + Day::test(Day::part1, "test-1", 33) + } + #[test] + fn part1_solution() -> Result<()> { + Day::test(Day::part1, "input", 600) + } + #[test] + fn part2_test1() -> Result<()> { + Day::test(Day::part2, "test-1", 56*62) + } + #[test] + fn part2_solution() -> Result<()> { + Day::test(Day::part2, "input", 6000) + } + + // Benchmarks + extern crate test; + #[bench] + #[ignore] + fn part1_bench(b: &mut test::Bencher) { + Day::benchmark(Day::part1, b) + } + #[bench] + #[ignore] + fn part2_bench(b: &mut test::Bencher) { + Day::benchmark(Day::part2, b) + } +} + +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +struct Resources { + ore: isize, + clay: isize, + obsidian: isize, +} + +impl Resources { + fn new(ore: isize, clay: isize, obsidian: isize) -> Self { + Self { ore, clay, obsidian } + } + + fn enough_for(&self, other: &Resources) -> bool { + self.ore >= other.ore && + self.clay >= other.clay && + self.obsidian >= other.obsidian + } +} + +impl AddAssign for Resources { + fn add_assign(&mut self, rhs: Self) { + self.ore += rhs.ore; + self.clay += rhs.clay; + self.obsidian += rhs.obsidian; + } +} + +impl SubAssign for Resources { + fn sub_assign(&mut self, rhs: Self) { + self.ore -= rhs.ore; + self.clay -= rhs.clay; + self.obsidian -= rhs.obsidian; + } +} + +#[derive(Debug)] +struct Blueprint { + ore_robot_cost: Resources, + clay_robot_cost: Resources, + obsidian_robot_cost: Resources, + geode_robot_cost: Resources, + max: Resources, +} + +impl Blueprint { + fn new(ore_robot_cost: Resources, clay_robot_cost: Resources, obsidian_robot_cost: Resources, geode_robot_cost: Resources) -> Self { + // Calculate how much of each resource we need at most every minute in order to construct + // every robot + let mut max = Resources::new(0, 0, 0); + + max.ore = max.ore.max(ore_robot_cost.ore).max(clay_robot_cost.ore).max(obsidian_robot_cost.ore).max(geode_robot_cost.ore); + max.clay = max.clay.max(ore_robot_cost.clay).max(clay_robot_cost.clay).max(obsidian_robot_cost.clay).max(geode_robot_cost.clay); + max.obsidian = max.obsidian.max(ore_robot_cost.obsidian).max(clay_robot_cost.obsidian).max(obsidian_robot_cost.obsidian).max(geode_robot_cost.obsidian); + + Self { ore_robot_cost, clay_robot_cost, obsidian_robot_cost, geode_robot_cost, max } + } +} + +impl FromStr for Blueprint { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let mut split = s.split(" "); + + let ore = Resources::new( + split.nth(6).unwrap().parse()?, + 0, + 0, + ); + + let clay = Resources::new( + split.nth(5).unwrap().parse()?, + 0, + 0, + ); + + let obsidian = Resources::new( + split.nth(5).unwrap().parse()?, + split.nth(2).unwrap().parse()?, + 0, + ); + + let geode = Resources::new( + split.nth(5).unwrap().parse()?, + 0, + split.nth(2).unwrap().parse()?, + ); + + Ok(Blueprint::new(ore, clay, obsidian, geode)) + } +} + +impl Blueprint { + fn visit(&self, state: State, mut best: isize) -> isize { + // If we have run out of time return the amount of geodes we have broken in total + if state.time_remaining <= 0 { + return state.geodes; + } + + // Using n(n+1)/2 calculate the maximum possible geodes that we can crack + // If we can not improve the best so far, we end here + // !!! This make a massive difference, from who knows how long to less then a second + if state.geodes + (state.time_remaining-1) * state.time_remaining / 2 < best { + return 0; + } + + // Given the remaining time, calculate how much of the resource we still need in the worst + // case scenerio. If current stockpile + future production if lower we will attempt to + // construct a robot, otherwise we do not need them anymore + // === Ore === + if state.robots.ore * state.time_remaining + state.resources.ore < self.max.ore * state.time_remaining { + // Check if we can construct one right now + if state.resources.enough_for(&self.ore_robot_cost) { + // Create a new state + let mut next = state.next(); + // Substract the resources + next.resources -= self.ore_robot_cost; + // Add the robot + next.robots.ore += 1; + + best = best.max(self.visit(next, best)); + } else { + // If we can not construct one right now, skip to the next point in time that we + // can build one + let mut next = state.next(); + while !next.resources.enough_for(&self.ore_robot_cost) && next.time_remaining >= 0 { + next = next.next(); + } + + best = best.max(self.visit(next, best)); + } + } + + // === Clay === + if state.robots.clay * state.time_remaining + state.resources.clay < self.max.clay * state.time_remaining { + if state.resources.enough_for(&self.clay_robot_cost) { + // Create a new state + let mut next = state.next(); + // Substract the resources + next.resources -= self.clay_robot_cost; + // Add the robot + next.robots.clay += 1; + + best = best.max(self.visit(next, best)); + } else { + // If we can not construct one right now, skip to the next point in time that we + // can build one + let mut next = state.next(); + while !next.resources.enough_for(&self.clay_robot_cost) && next.time_remaining >= 0 { + next = next.next(); + } + + best = best.max(self.visit(next, best)); + } + } + + // === Obsidian === + if state.robots.obsidian * state.time_remaining + state.resources.obsidian < self.max.obsidian * state.time_remaining { + if state.resources.enough_for(&self.obsidian_robot_cost) { + // Create a new state + let mut next = state.next(); + // Substract the resources + next.resources -= self.obsidian_robot_cost; + // Add the robot + next.robots.obsidian += 1; + + best = best.max(self.visit(next, best)); + } else { + // If we can not construct one right now, skip to the next point in time that we + // can build one + let mut next = state.next(); + while !next.resources.enough_for(&self.obsidian_robot_cost) && next.time_remaining >= 0 { + next = next.next(); + } + + best = best.max(self.visit(next, best)); + } + } + + // === Geode == + // There is no upper limit to producing robots, so only check if we can make one + if state.resources.enough_for(&self.geode_robot_cost) { + // Create a new state + let mut next = state.next(); + // Substract the resources + next.resources -= self.geode_robot_cost; + // Since we never use the geodes we can add all that this robot will mine at once + next.geodes += state.time_remaining - 1; + + best = best.max(self.visit(next, best)); + } else { + // If we can not construct one right now, skip to the next point in time that we + // can build one + let mut next = state.next(); + while !next.resources.enough_for(&self.geode_robot_cost) && next.time_remaining >= 0 { + next = next.next(); + } + + best = best.max(self.visit(next, best)); + } + + return best; + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +struct State { + resources: Resources, + robots: Resources, + geodes: isize, + time_remaining: isize, +} + +impl State { + fn new(time_remaining: isize) -> Self { + Self { + resources: Resources::new(0, 0, 0), + robots: Resources::new(1, 0, 0), + geodes: 0, + time_remaining, + } + } + + fn next(&self) -> Self { + let mut next = self.clone(); + + // Update the time + next.time_remaining -= 1; + // Collect resources + next.resources += self.robots; + + return next; + } +} + +// -- Solution -- +pub struct Day; +impl aoc::Solver for Day { + type Output1 = isize; + type Output2 = isize; + + fn day() -> u8 { + 19 + } + + fn part1(input: &str) -> Self::Output1 { + let blueprints = input.trim().lines().flat_map(Blueprint::from_str).collect::>(); + + let state = State::new(24); + blueprints.iter() + .enumerate() + .map(|(idx, blueprint)| { + blueprint.visit(state.clone(), 0) * (idx as isize + 1) + }).sum() + } + + fn part2(input: &str) -> Self::Output2 { + let blueprints = input.trim().lines().flat_map(Blueprint::from_str).collect::>(); + let state = State::new(32); + blueprints.iter() + .take(3) + .fold(1, |acc, blueprint| { + acc * blueprint.visit(state.clone(), 0) + }) + } +}