2022 Day 19 Part 1
This commit is contained in:
parent
b9661d77be
commit
24c827bac8
1 changed files with 380 additions and 0 deletions
380
2022/src/bin/day19.rs
Normal file
380
2022/src/bin/day19.rs
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
use std::{
|
||||
collections::{BTreeMap, HashMap, VecDeque},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use regex::Regex;
|
||||
use serde::{
|
||||
de::{value, IntoDeserializer},
|
||||
Deserialize,
|
||||
};
|
||||
|
||||
const INPUT: &str = include_str!("../../input/day19.txt");
|
||||
|
||||
fn main() {
|
||||
let blueprints: Vec<Blueprint> = aoc::lines(INPUT).map(Blueprint::parse).collect();
|
||||
println!("{}", part1(blueprints));
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Blueprint {
|
||||
id: usize,
|
||||
recipes: Vec<Recipe>,
|
||||
}
|
||||
|
||||
impl Blueprint {
|
||||
fn parse(line: &str) -> Self {
|
||||
let id: usize = {
|
||||
let re = Regex::new(r"^Blueprint (\d+):").unwrap();
|
||||
let caps = re.captures(line).unwrap();
|
||||
caps[1].parse().unwrap()
|
||||
};
|
||||
|
||||
let recipes = Recipe::parse_all(line);
|
||||
|
||||
Self { id, recipes }
|
||||
}
|
||||
|
||||
fn id(&self) -> u64 {
|
||||
self.id.try_into().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct Recipe {
|
||||
resource: Resource,
|
||||
costs: HashMap<Resource, u64>,
|
||||
}
|
||||
|
||||
impl Recipe {
|
||||
fn parse_all(line: &str) -> Vec<Self> {
|
||||
let re = Regex::new(r"Each (.*?) robot costs ([^.]*).").unwrap();
|
||||
|
||||
let mut recipes = vec![];
|
||||
for caps in re.captures_iter(line) {
|
||||
let resource = Resource::from_str(&caps[1]).unwrap();
|
||||
|
||||
let mut costs = HashMap::new();
|
||||
for phrase in caps[2].split(" and ") {
|
||||
let words: Vec<&str> = aoc::words(phrase).collect();
|
||||
let n: u64 = words[0].parse().unwrap();
|
||||
let res = Resource::from_str(words[1]).unwrap();
|
||||
|
||||
let old = costs.insert(res, n);
|
||||
assert!(old.is_none());
|
||||
}
|
||||
|
||||
recipes.push(Self { resource, costs });
|
||||
}
|
||||
|
||||
recipes
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum Resource {
|
||||
Ore,
|
||||
Clay,
|
||||
Obsidian,
|
||||
Geode,
|
||||
}
|
||||
|
||||
impl Resource {
|
||||
const ALL: [Resource; 4] = [Self::Ore, Self::Clay, Self::Obsidian, Self::Geode];
|
||||
}
|
||||
|
||||
impl FromStr for Resource {
|
||||
type Err = value::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::deserialize(s.into_deserializer())
|
||||
}
|
||||
}
|
||||
|
||||
fn part1(blueprints: Vec<Blueprint>) -> u64 {
|
||||
blueprints
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|bp| bp.id() * quality(bp, 24))
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn quality(blueprint: Blueprint, total_minutes: u64) -> u64 {
|
||||
dbg!(&blueprint.id);
|
||||
|
||||
let factory = Factory::new(blueprint);
|
||||
|
||||
let mut queue: VecDeque<State> = VecDeque::new();
|
||||
|
||||
let start = State::start(total_minutes);
|
||||
queue.push_front(start.clone());
|
||||
|
||||
let mut bests: DefaultDict<u64, u64> = DefaultDict::new();
|
||||
bests.insert(start.minutes_left, start.geodes());
|
||||
|
||||
while let Some(state) = queue.pop_front() {
|
||||
let best_known = bests.get(&state.minutes_left);
|
||||
if state.optimistic_geodes() <= best_known {
|
||||
// This branch of the tree can't possibly do better.
|
||||
continue;
|
||||
}
|
||||
|
||||
bests.modify(state.minutes_left, |best| best.max(state.geodes()));
|
||||
|
||||
for next in state.successors(&factory) {
|
||||
queue.push_front(next);
|
||||
}
|
||||
}
|
||||
|
||||
bests.get(&0)
|
||||
}
|
||||
|
||||
impl Blueprint {
|
||||
fn robot_costs(&self) -> HashMap<Resource, HashMap<Resource, u64>> {
|
||||
let mut costs = HashMap::new();
|
||||
|
||||
for robot_type in Resource::ALL {
|
||||
costs.insert(robot_type, self.costs(robot_type));
|
||||
}
|
||||
|
||||
costs
|
||||
}
|
||||
|
||||
fn costs(&self, robot_type: Resource) -> HashMap<Resource, u64> {
|
||||
let recipe = self
|
||||
.recipes
|
||||
.iter()
|
||||
.find(|r| r.resource == robot_type)
|
||||
.unwrap();
|
||||
recipe.costs.clone()
|
||||
}
|
||||
|
||||
fn max_usable(&self) -> DefaultDict<Resource, u64> {
|
||||
let mut max: DefaultDict<Resource, u64> = DefaultDict::new();
|
||||
for r in &self.recipes {
|
||||
for (&res, &amount) in &r.costs {
|
||||
let old = max.get(&res);
|
||||
max.insert(res, old.max(amount));
|
||||
}
|
||||
}
|
||||
max
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Factory {
|
||||
robot_costs: HashMap<Resource, HashMap<Resource, u64>>,
|
||||
max_usable: DefaultDict<Resource, u64>,
|
||||
}
|
||||
|
||||
impl Factory {
|
||||
fn new(blueprint: Blueprint) -> Self {
|
||||
Self {
|
||||
robot_costs: blueprint.robot_costs(),
|
||||
max_usable: blueprint.max_usable(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Factory {
|
||||
fn can_build(&self, robot_type: Resource, items: &DefaultDict<Resource, u64>) -> bool {
|
||||
let costs = self.robot_costs.get(&robot_type).unwrap();
|
||||
|
||||
costs.iter().all(|(&res, &want)| {
|
||||
let have = items.get(&res);
|
||||
have >= want
|
||||
})
|
||||
}
|
||||
|
||||
fn should_build(&self, robot_type: Resource, robots: &DefaultDict<Resource, u64>) -> bool {
|
||||
if robot_type == Resource::Geode {
|
||||
return true;
|
||||
}
|
||||
|
||||
let production = robots.get(&robot_type);
|
||||
let usage = self.max_usable.get(&robot_type);
|
||||
|
||||
production < usage
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct State {
|
||||
minutes_left: u64,
|
||||
|
||||
robots: DefaultDict<Resource, u64>,
|
||||
items: DefaultDict<Resource, u64>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for State {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "Robots: {:?}", self.robots)?;
|
||||
writeln!(f, "Items: {:?}", self.items)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn start(minutes_left: u64) -> Self {
|
||||
let mut robots = DefaultDict::new();
|
||||
robots.insert(Resource::Ore, 1);
|
||||
|
||||
Self {
|
||||
minutes_left,
|
||||
robots,
|
||||
items: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn geodes(&self) -> u64 {
|
||||
self.items.get(&Resource::Geode)
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn tick(&mut self, factory: &Factory, robot_type: Option<Resource>) {
|
||||
if let Some(robot_type) = robot_type {
|
||||
self.start_building(factory, robot_type);
|
||||
}
|
||||
|
||||
self.mine();
|
||||
|
||||
if let Some(robot_type) = robot_type {
|
||||
self.add_robot(robot_type);
|
||||
}
|
||||
|
||||
self.minutes_left -= 1;
|
||||
}
|
||||
|
||||
fn start_building(&mut self, factory: &Factory, robot_type: Resource) {
|
||||
for (&res, &cost) in factory.robot_costs.get(&robot_type).unwrap() {
|
||||
self.items.modify(res, |have| have - cost);
|
||||
}
|
||||
}
|
||||
|
||||
fn mine(&mut self) {
|
||||
for (&res, &mined) in &self.robots.0 {
|
||||
self.items.modify(res, |have| have + mined);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_robot(&mut self, robot_type: Resource) {
|
||||
self.robots.modify(robot_type, |have| have + 1);
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn optimistic_geodes(&self) -> u64 {
|
||||
let dt = self.minutes_left;
|
||||
|
||||
let geode_rate = self.robots.get(&Resource::Geode);
|
||||
let guaranteed = geode_rate * dt;
|
||||
|
||||
// Build a geode miner every turn and sum the extra outputs.
|
||||
let magic_mining = triangle(dt);
|
||||
|
||||
self.geodes() + guaranteed + magic_mining
|
||||
}
|
||||
|
||||
fn successors(&self, factory: &Factory) -> Vec<Self> {
|
||||
// The search should never try this, but just in case...
|
||||
if self.minutes_left == 0 {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
// There are exactly five possible worlds:
|
||||
let mut states = vec![];
|
||||
|
||||
// 1-4: Build a robot of type X.
|
||||
//
|
||||
// By assumption, all timesteps between now and the actual robot build step _must_ be
|
||||
// idling steps.
|
||||
for robot_type in Resource::ALL {
|
||||
// Avoid building a robot whose output would never get used.
|
||||
if !factory.should_build(robot_type, &self.robots) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut next = self.clone();
|
||||
|
||||
// Idle-mine until the robot can be built.
|
||||
//
|
||||
// TODO: I'm sure there's a simple inequality for this.
|
||||
while next.minutes_left > 0 && !factory.can_build(robot_type, &next.items) {
|
||||
next.tick(factory, None);
|
||||
}
|
||||
|
||||
// Build the robot if there's any time left.
|
||||
if next.minutes_left > 0 {
|
||||
next.tick(factory, Some(robot_type));
|
||||
states.push(next);
|
||||
}
|
||||
}
|
||||
|
||||
// 5: Never build a robot again, ever.
|
||||
//
|
||||
// Skip directly to the end by idle-mining all the time away.
|
||||
{
|
||||
let mut next = self.clone();
|
||||
while next.minutes_left > 0 {
|
||||
next.tick(factory, None);
|
||||
}
|
||||
states.push(next);
|
||||
}
|
||||
|
||||
states
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct DefaultDict<K, V>(BTreeMap<K, V>)
|
||||
where
|
||||
K: Eq + std::hash::Hash,
|
||||
V: Default + Clone;
|
||||
|
||||
impl<K, V> DefaultDict<K, V>
|
||||
where
|
||||
K: Eq + std::hash::Hash + std::cmp::Ord,
|
||||
V: Default + Clone,
|
||||
{
|
||||
fn new() -> Self {
|
||||
Self(BTreeMap::new())
|
||||
}
|
||||
|
||||
fn insert(&mut self, key: K, val: V) -> Option<V> {
|
||||
self.0.insert(key, val)
|
||||
}
|
||||
|
||||
fn get(&self, key: &K) -> V {
|
||||
if let Some(v) = self.0.get(key) {
|
||||
v.clone()
|
||||
} else {
|
||||
V::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn modify<F>(&mut self, key: K, f: F) -> V
|
||||
where
|
||||
F: FnOnce(V) -> V,
|
||||
{
|
||||
let old = self.get(&key);
|
||||
let new = f(old);
|
||||
self.insert(key, new.clone());
|
||||
new
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> Default for DefaultDict<K, V>
|
||||
where
|
||||
K: Eq + std::hash::Hash,
|
||||
V: Default + Clone,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
fn triangle(n: u64) -> u64 {
|
||||
(n * (n + 1)) / 2
|
||||
}
|
||||
Loading…
Reference in a new issue