From ab870a881ef3092c185e56ca8fe59d1e92943184 Mon Sep 17 00:00:00 2001 From: jazzpi Date: Wed, 21 Dec 2022 13:39:43 +0100 Subject: [PATCH] Day 19, part 1 --- src/bin/d19p1.rs | 26 ++++++ src/day19.rs | 238 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 3 files changed, 265 insertions(+) create mode 100644 src/bin/d19p1.rs create mode 100644 src/day19.rs diff --git a/src/bin/d19p1.rs b/src/bin/d19p1.rs new file mode 100644 index 0000000..7e7e7e7 --- /dev/null +++ b/src/bin/d19p1.rs @@ -0,0 +1,26 @@ +use std::thread; + +use aoc22::{day19, util}; + +const MINUTES: usize = 24; + +pub fn main() { + let blueprints = day19::parse_blueprints(&util::parse_input()); + + let mut handles = Vec::new(); + for (i, blueprint) in blueprints.iter().enumerate() { + let blueprint = blueprint.clone(); + handles.push(( + i, + thread::spawn(move || day19::max_geodes(MINUTES, &blueprint)), + )); + } + + let mut sum = 0; + for (i, handle) in handles { + let max = handle.join().unwrap(); + sum += (i + 1) * max; + } + + println!("Sum of quality scores: {}", sum); +} diff --git a/src/day19.rs b/src/day19.rs new file mode 100644 index 0000000..23749b8 --- /dev/null +++ b/src/day19.rs @@ -0,0 +1,238 @@ +use std::collections::HashSet; + +use regex::Regex; + +#[derive(Debug, Clone)] +pub struct Blueprint { + pub ore_ore_cost: usize, + pub clay_ore_cost: usize, + pub obsidian_ore_cost: usize, + pub obsidian_clay_cost: usize, + pub geode_ore_cost: usize, + pub geode_obsidian_cost: usize, +} + +pub fn parse_blueprints(input: &String) -> Vec { + let re = Regex::new( + r"Blueprint (\d+): Each ore robot costs (\d+) ore. Each clay robot costs (\d+) ore. Each obsidian robot costs (\d+) ore and (\d+) clay. Each geode robot costs (\d+) ore and (\d+) obsidian.", + ).unwrap(); + input + .lines() + .enumerate() + .map(|(i, line)| { + let captures = re + .captures(line) + .unwrap_or_else(|| panic!("Invalid blueprint formatting:\n{}", line)); + assert_eq!( + captures.get(1).unwrap().as_str().parse::().unwrap(), + i + 1 + ); + + let ore_ore_cost = captures.get(2).unwrap().as_str().parse().unwrap(); + let clay_ore_cost = captures.get(3).unwrap().as_str().parse().unwrap(); + let obsidian_ore_cost = captures.get(4).unwrap().as_str().parse().unwrap(); + let obsidian_clay_cost = captures.get(5).unwrap().as_str().parse().unwrap(); + let geode_ore_cost = captures.get(6).unwrap().as_str().parse().unwrap(); + let geode_obsidian_cost = captures.get(7).unwrap().as_str().parse().unwrap(); + + Blueprint { + ore_ore_cost, + clay_ore_cost, + obsidian_ore_cost, + obsidian_clay_cost, + geode_ore_cost, + geode_obsidian_cost, + } + }) + .collect() +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct State { + pub time_remaining: usize, + + pub ore: usize, + pub clay: usize, + pub obsidian: usize, + pub geodes: usize, + + pub ore_robots: usize, + pub clay_robots: usize, + pub obsidian_robots: usize, + pub geode_robots: usize, +} + +impl State { + pub fn new(initial_time: usize) -> State { + State { + time_remaining: initial_time, + ore: 0, + clay: 0, + obsidian: 0, + geodes: 0, + ore_robots: 1, + clay_robots: 0, + obsidian_robots: 0, + geode_robots: 0, + } + } + + pub fn finished(&self) -> bool { + self.time_remaining == 0 + } + + pub fn possible_actions(&self, blueprint: &Blueprint) -> Vec { + assert!(!self.finished()); + + let mut result = Vec::new(); + + if self.time_remaining > 1 { + self.produce_ore_next(blueprint).map(|a| result.push(a)); + self.produce_clay_next(blueprint).map(|a| result.push(a)); + self.produce_obsidian_next(blueprint) + .map(|a| result.push(a)); + self.produce_geode_next(blueprint).map(|a| result.push(a)); + } + let mut do_nothing = self.clone(); + do_nothing.run_steps(self.time_remaining); + result.push(do_nothing); + + result + } + + fn produce(&self, time_for_ore_prod: isize, produce: F) -> Option { + let time_until_robot_ready = (time_for_ore_prod.max(0) as usize) + 1; + // For this to make sense, we also need at least one minute + // remaining after producing the robot + if time_until_robot_ready + 1 > self.time_remaining { + return None; + } + + let mut produced = self.clone(); + produced.run_steps(time_until_robot_ready); + produce(&mut produced); + Some(produced) + } + + fn produce_ore_next(&self, blueprint: &Blueprint) -> Option { + if self.ore_robots == 0 { + return None; + } + let time_required = num::Integer::div_ceil( + &((blueprint.ore_ore_cost as isize) - (self.ore as isize)), + &(self.ore_robots as isize), + ); + self.produce(time_required, |mut s| { + s.ore_robots += 1; + s.ore -= blueprint.ore_ore_cost; + }) + } + + fn produce_clay_next(&self, blueprint: &Blueprint) -> Option { + if self.ore_robots == 0 { + return None; + } + let time_required = num::Integer::div_ceil( + &((blueprint.clay_ore_cost as isize) - (self.ore as isize)), + &(self.ore_robots as isize), + ); + self.produce(time_required, |mut s| { + s.clay_robots += 1; + s.ore -= blueprint.clay_ore_cost; + }) + } + + fn produce_obsidian_next(&self, blueprint: &Blueprint) -> Option { + if self.ore_robots == 0 || self.clay_robots == 0 { + return None; + } + let time_required_ore = num::Integer::div_ceil( + &((blueprint.obsidian_ore_cost as isize) - (self.ore as isize)), + &(self.ore_robots as isize), + ); + let time_required_clay = num::Integer::div_ceil( + &((blueprint.obsidian_clay_cost as isize) - (self.clay as isize)), + &(self.clay_robots as isize), + ); + self.produce(time_required_ore.max(time_required_clay), |mut s| { + s.obsidian_robots += 1; + s.ore -= blueprint.obsidian_ore_cost; + s.clay -= blueprint.obsidian_clay_cost; + }) + } + + fn produce_geode_next(&self, blueprint: &Blueprint) -> Option { + if self.ore_robots == 0 || self.obsidian_robots == 0 { + return None; + } + let time_required_ore = num::Integer::div_ceil( + &((blueprint.geode_ore_cost as isize) - (self.ore as isize)), + &(self.ore_robots as isize), + ); + let time_required_obsidian = num::Integer::div_ceil( + &((blueprint.geode_obsidian_cost as isize) - (self.obsidian as isize)), + &(self.obsidian_robots as isize), + ); + self.produce(time_required_ore.max(time_required_obsidian), |mut s| { + s.geode_robots += 1; + s.ore -= blueprint.geode_ore_cost; + s.obsidian -= blueprint.geode_obsidian_cost; + }) + } + + pub fn upper_bound(&self) -> usize { + // Build one geode robot each remaining turn + // \sum_{k=1}^n {k - 1} = \sum_{k=0}^{n-1} {k} = 1/2 (n-1) n + self.lower_bound() + ((self.time_remaining - 1) * self.time_remaining) / 2 + } + + pub fn lower_bound(&self) -> usize { + self.geodes + self.geode_robots * self.time_remaining + } + + fn run_steps(&mut self, n: usize) { + assert!(self.time_remaining >= n); + + self.time_remaining -= n; + self.ore += self.ore_robots * n; + self.clay += self.clay_robots * n; + self.obsidian += self.obsidian_robots * n; + self.geodes += self.geode_robots * n; + } +} + +pub fn max_geodes(minutes: usize, blueprint: &Blueprint) -> usize { + let initial = State::new(minutes); + let initial_upper = initial.upper_bound(); + + let mut next = Vec::new(); + next.push((initial.clone(), initial_upper)); + let mut visited = HashSet::new(); + visited.insert(initial); + + let mut lower_bound = 0; + while let Some(n) = next.pop() { + if n.1 < lower_bound { + // Between pushing this state and popping it, we've found a better lower bound + continue; + } + + let state = n.0; + for action in state.possible_actions(blueprint) { + if action.finished() { + let action_lower = action.lower_bound(); + if action_lower > lower_bound { + lower_bound = action_lower; + } + } else { + let action_upper = action.upper_bound(); + if action_upper > lower_bound && !visited.contains(&action) { + next.push((action.clone(), action_upper)); + visited.insert(action); + } + } + } + } + + lower_bound +} diff --git a/src/lib.rs b/src/lib.rs index 8ca45b8..c978299 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ pub mod day15; pub mod day16; pub mod day17; pub mod day18; +pub mod day19; pub mod day2; pub mod day3; pub mod day4;