From 302020e79f14e74bf35f9d9779fcf37f518426c8 Mon Sep 17 00:00:00 2001 From: Rob Tsuk Date: Fri, 23 Dec 2022 08:47:01 -0700 Subject: [PATCH] Day 19 --- .gitignore | 1 + Cargo.toml | 2 + src/bin/day17.rs | 1 + src/bin/day19.rs | 593 +++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 546 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index 4c790d0..326866f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /Cargo.lock .DS_Store +/states.txt diff --git a/Cargo.toml b/Cargo.toml index f380197..c2437c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] anyhow = "1.0.66" console = "0.15.2" +enum-iterator = "1.2.0" euclid = { version = "0.22.7", features = ["serde"] } internment = "0.7.0" itertools = "0.10.5" @@ -16,5 +17,6 @@ once_cell = "1.16.0" pathfinding = "4.0.0" petgraph = "0.6.2" ranges = "0.3.3" +rayon = "1.6.1" regex = "1.7.0" structopt = "0.3.26" diff --git a/src/bin/day17.rs b/src/bin/day17.rs index ca12984..1608cf7 100644 --- a/src/bin/day17.rs +++ b/src/bin/day17.rs @@ -20,6 +20,7 @@ struct Opt { /// Limit #[structopt(short, long, default_value = "2022")] + #[allow(unused)] limit: usize, } diff --git a/src/bin/day19.rs b/src/bin/day19.rs index b234b22..23e31eb 100644 --- a/src/bin/day19.rs +++ b/src/bin/day19.rs @@ -1,8 +1,25 @@ use anyhow::Error; +use enum_iterator::{all, Sequence}; +use itertools::Itertools; +use rayon::prelude::*; use regex::Regex; -use std::ops::{Add, AddAssign, Mul, Sub}; +use std::{ + collections::BTreeSet, + ops::{Add, AddAssign, Mul, Range, Sub}, +}; use structopt::StructOpt; +#[repr(usize)] +#[derive(Debug, Clone, Copy, PartialEq, Sequence)] +enum ResourceType { + Ore, + Clay, + Obsidian, + Geode, +} + +type ResourceCount = usize; + const DATA: &str = include_str!("../../data/day19.txt"); const SAMPLE: &str = r#"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. @@ -14,20 +31,39 @@ struct Opt { /// Use puzzle input instead of the sample #[structopt(short, long)] puzzle_input: bool, + + #[structopt(long, default_value = "24")] + time_limit: usize, + + #[structopt(long, default_value = "2000")] + blueprint_limit: usize, } -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd, Ord, Hash, Eq)] struct Resources { - ore: usize, - clay: usize, - obsidian: usize, - geode: usize, + geode: ResourceCount, + obsidian: ResourceCount, + clay: ResourceCount, + ore: ResourceCount, } -impl Mul for Resources { +impl Resources { + fn contains(&self, other: &Resources) -> bool { + self.ore >= other.ore + && self.clay >= other.clay + && self.obsidian >= other.obsidian + && self.geode >= other.geode + } + + fn total_resources(&self) -> ResourceCount { + self.ore + self.clay + self.obsidian + self.geode + } +} + +impl Mul for Resources { type Output = Self; - fn mul(self, rhs: usize) -> Self { + fn mul(self, rhs: ResourceCount) -> Self { Self { ore: self.ore * rhs, clay: self.clay * rhs, @@ -74,6 +110,49 @@ impl AddAssign for Resources { } } +#[derive(Debug, Default, Hash, PartialEq, Eq)] +struct RobotDelivery { + time: usize, + robots: Robots, +} + +type StateSet = BTreeSet; + +#[derive(Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +struct State { + robots: Robots, + resources: Resources, +} + +impl State { + fn starting() -> Self { + Self { + robots: Robots { + ore: 1, + ..Default::default() + }, + ..Default::default() + } + } + + fn with_order(&self, bp: &Blueprint, _time: usize, robot_order: Robots) -> Self { + let mut resources = self.resources - bp.build_cost(&robot_order); + resources += resources_made(&self.robots); + let robots = self.robots + robot_order; + + Self { robots, resources } + } + + fn step(&self, bp: &Blueprint, time: usize, _limit: usize) -> StateSet { + let orders = order_permutation_s(&self.resources, &self.robots, bp); + + orders + .into_iter() + .map(|o| self.with_order(bp, time, o)) + .collect() + } +} + #[derive(Debug, Default)] struct Blueprint { id: usize, @@ -105,9 +184,27 @@ impl Blueprint { obsidian: parts[7].parse().unwrap(), ..Resources::default() }, - ..Self::default() } } + + fn robot_cost(&self, resource_type: ResourceType) -> Resources { + match resource_type { + ResourceType::Ore => self.ore_robot, + ResourceType::Clay => self.clay_robot, + ResourceType::Obsidian => self.obsidian_robot, + ResourceType::Geode => self.geode_robot, + } + } + + fn build_cost(&self, robots: &Robots) -> Resources { + let mut cost = Default::default(); + for rt in all::() { + if robots.contains(rt) { + cost += self.robot_cost(rt); + } + } + cost + } } fn parse(s: &str) -> Vec { @@ -116,15 +213,26 @@ fn parse(s: &str) -> Vec { "#, ).expect("re"); - re.captures_iter(s).map(|c| Blueprint::new(c)).collect() + re.captures_iter(s).map(Blueprint::new).collect() } -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq, Clone, Copy, Hash, Eq, PartialOrd, Ord)] struct Robots { - ore: usize, - clay: usize, - obsidian: usize, - geode: usize, + geode: ResourceCount, + obsidian: ResourceCount, + clay: ResourceCount, + ore: ResourceCount, +} + +impl Robots { + fn contains(&self, resource_type: ResourceType) -> bool { + match resource_type { + ResourceType::Ore => self.ore > 0, + ResourceType::Clay => self.clay > 0, + ResourceType::Obsidian => self.obsidian > 0, + ResourceType::Geode => self.geode > 0, + } + } } impl Add for Robots { @@ -135,7 +243,7 @@ impl Add for Robots { ore: self.ore.checked_add(other.ore).unwrap(), clay: self.clay.checked_add(other.clay).unwrap(), obsidian: self.obsidian.checked_add(other.obsidian).unwrap(), - geode: self.geode.checked_add(other.obsidian).unwrap(), + geode: self.geode.checked_add(other.geode).unwrap(), } } } @@ -151,28 +259,89 @@ impl AddAssign for Robots { } } -fn legal_order(resources: Resources, blueprint: &Blueprint) -> (Robots, Resources) { - let mut robots = Robots::default(); - - let max_geode = (resources.obsidian / blueprint.geode_robot.obsidian) - .min(resources.obsidian / blueprint.geode_robot.obsidian); - let resources = resources - blueprint.geode_robot * max_geode; - robots.geode = max_geode; - - let max_obsidian = (resources.ore / blueprint.obsidian_robot.ore) - .min(resources.clay / blueprint.obsidian_robot.clay); - let resources = resources - blueprint.obsidian_robot * max_obsidian; - robots.obsidian = max_obsidian; - - let max_clay = resources.ore / blueprint.clay_robot.ore; - let resources = resources - blueprint.clay_robot * max_clay; - robots.clay = max_clay; +fn order_permutation_s( + resources: &Resources, + _robots: &Robots, + blueprint: &Blueprint, +) -> Vec { + let possible_builds = vec![ + Robots::default(), + Robots { + geode: 1, + ..Robots::default() + }, + Robots { + ore: 1, + ..Robots::default() + }, + Robots { + clay: 1, + ..Robots::default() + }, + Robots { + obsidian: 1, + ..Robots::default() + }, + ]; + let mut p = vec![]; + for r in possible_builds.iter() { + let cost = blueprint.build_cost(r); + if resources.contains(&cost) { + p.push(*r); + } + } - let max_ore = resources.ore / blueprint.ore_robot.ore; - let resources = resources - blueprint.ore_robot * max_ore; - robots.ore = max_ore; + p +} - (robots, resources) +#[allow(unused)] +fn order_permutation(resources: &Resources, robots: &Robots, blueprint: &Blueprint) -> Vec { + const ZERO_OR_ONE: Range = 0..2; + let mut p = vec![]; + let max_clay = if robots.clay < blueprint.obsidian_robot.clay { + 2 + } else { + 1 + }; + let max_ore = if robots.ore + < (blueprint.obsidian_robot.ore + + blueprint.geode_robot.ore + + blueprint.clay_robot.ore + + blueprint.ore_robot.ore) + { + 2 + } else { + 1 + }; + let max_obsidian = if robots.obsidian < blueprint.geode_robot.obsidian { + 2 + } else { + 1 + }; + for geode in ZERO_OR_ONE { + for obsidian in 0..max_obsidian { + for clay in 0..max_clay { + for ore in 0..max_ore { + let robots = Robots { + ore, + clay, + obsidian, + geode, + }; + let cost = blueprint.build_cost(&Robots { + ore, + clay, + obsidian, + geode, + }); + if resources.contains(&cost) { + p.push(robots); + } + } + } + } + } + p } fn resources_made(robots: &Robots) -> Resources { @@ -189,24 +358,346 @@ fn main() -> Result<(), Error> { let blueprints = parse(if opt.puzzle_input { DATA } else { SAMPLE }); - let bp = &blueprints[0]; + let mut quality_level = 0; + let mut total = 1; + let blueprint_limit = opt.blueprint_limit.min(blueprints.len()); + for bp in &blueprints[0..blueprint_limit] { + let mut states: StateSet = StateSet::new(); + states.insert(State::starting()); - let mut robots = Robots { - ore: 1, - ..Default::default() - }; - let mut resources = Resources::default(); + for time in 1..=opt.time_limit { + println!("### time = {time} state count = {}", states.len()); + let new_states: StateSet = states + .par_iter() + .flat_map(|state| state.step(bp, time, opt.time_limit)) + .collect(); + + let mut new_state_pared = StateSet::new(); + for (_key, group) in &new_states.iter().group_by(|s| s.robots) { + let mut state_group = group.collect::>(); + state_group.sort_by_key(|s| s.resources.total_resources()); + state_group.reverse(); + for state in &state_group[0..10.min(state_group.len())] { + new_state_pared.insert(**state); + } + } + states = new_state_pared; + } + + println!("done"); - for time in 1..=24 { - println!("#### time {time}: robots = {robots:#?} "); - resources += resources_made(&robots); - println!("resources = {resources:#?} "); - let (new_robots, remainder) = legal_order(resources, &bp); - println!("new_robots = {new_robots:#?}"); - println!("remainder = {remainder:#?}"); - robots += new_robots; - resources = remainder; + let mut state_list: Vec<_> = states.into_iter().collect(); + + state_list.sort_by_key(|s| s.resources); + state_list.reverse(); + let geodes = state_list[0].resources.geode; + println!("state = {:#?}", &state_list[0]); + quality_level += bp.id * geodes; + total *= geodes; } + println!("quality_level = {quality_level}"); + println!("total = {total}"); Ok(()) } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_parse() { + let bps = parse(SAMPLE); + dbg!(&bps); + assert_eq!(bps.len(), 2); + } + + #[test] + fn test_order_permutation() { + let bps = parse(SAMPLE); + let bp0 = &bps[0]; + + let r = Resources::default(); + let robots = Robots::default(); + let orders = order_permutation_s(&r, &robots, bp0); + + assert_eq!(orders.len(), 1); + + let r = Resources { + ore: 4, + ..Resources::default() + }; + + let orders = order_permutation_s(&r, &robots, bp0); + + dbg!(&orders); + assert_eq!(orders.len(), 3); + + let r = Resources { + ore: 8, + clay: 14, + ..Resources::default() + }; + + let orders = order_permutation_s(&r, &robots, bp0); + + assert_eq!(orders.len(), 4); + let r = Resources { + ore: 4, + clay: 15, + ..Resources::default() + }; + + let orders = order_permutation_s(&r, &robots, bp0); + + dbg!(&orders); + + assert_eq!(orders.len(), 4); + } + + #[test] + fn test_time_10() { + let bps = parse(SAMPLE); + let bp0 = &bps[0]; + + println!("bp = {:#?}", bp0); + + let state = State { + robots: Robots { + ore: 1, + clay: 3, + ..Robots::default() + }, + resources: Resources { + ore: 4, + clay: 15, + ..Resources::default() + }, + }; + + let expected_state = State { + robots: Robots { + ore: 1, + clay: 3, + obsidian: 1, + ..Robots::default() + }, + resources: Resources { + ore: 2, + clay: 4, + ..Resources::default() + }, + }; + let new_state = state.with_order( + bp0, + 10, + Robots { + obsidian: 1, + ..Robots::default() + }, + ); + + assert_eq!(new_state, expected_state); + } + + #[test] + fn test_solve() { + let bps = parse(SAMPLE); + let bp0 = &bps[0]; + + println!("bp = {:#?}", bp0); + + let expected_states: &[(usize, State)] = &[ + (0, State::starting()), + ( + 1, + State { + robots: Robots { + ore: 1, + ..Robots::default() + }, + resources: Resources { + ore: 1, + ..Resources::default() + }, + }, + ), + ( + 2, + State { + robots: Robots { + ore: 1, + ..Robots::default() + }, + resources: Resources { + ore: 2, + ..Resources::default() + }, + }, + ), + ( + 3, + State { + robots: Robots { + ore: 1, + clay: 1, + ..Robots::default() + }, + resources: Resources { + ore: 1, + clay: 0, + ..Resources::default() + }, + }, + ), + ( + 4, + State { + robots: Robots { + ore: 1, + clay: 1, + ..Robots::default() + }, + resources: Resources { + ore: 2, + clay: 1, + ..Resources::default() + }, + }, + ), + ( + 5, + State { + robots: Robots { + ore: 1, + clay: 2, + ..Robots::default() + }, + resources: Resources { + ore: 1, + clay: 2, + ..Resources::default() + }, + }, + ), + ( + 6, + State { + robots: Robots { + ore: 1, + clay: 2, + ..Robots::default() + }, + resources: Resources { + ore: 2, + clay: 4, + ..Resources::default() + }, + }, + ), + ( + 7, + State { + robots: Robots { + ore: 1, + clay: 3, + ..Robots::default() + }, + resources: Resources { + ore: 1, + clay: 6, + ..Resources::default() + }, + }, + ), + ( + 8, + State { + robots: Robots { + ore: 1, + clay: 3, + ..Robots::default() + }, + resources: Resources { + ore: 2, + clay: 9, + ..Resources::default() + }, + }, + ), + ( + 9, + State { + robots: Robots { + ore: 1, + clay: 3, + ..Robots::default() + }, + resources: Resources { + ore: 3, + clay: 12, + ..Resources::default() + }, + }, + ), + ( + 10, + State { + robots: Robots { + ore: 1, + clay: 3, + ..Robots::default() + }, + resources: Resources { + ore: 4, + clay: 15, + ..Resources::default() + }, + }, + ), + ( + 11, + State { + robots: Robots { + ore: 1, + clay: 3, + obsidian: 1, + ..Robots::default() + }, + resources: Resources { + ore: 2, + clay: 4, + ..Resources::default() + }, + }, + ), + ]; + + let mut states: StateSet = StateSet::new(); + states.insert(State::starting()); + + for (i, expected_state) in expected_states.iter().enumerate() { + let time = i + 1; + if !states.contains(&expected_state.1) { + println!("### time = {time}"); + let mut state_list: Vec<_> = states.into_iter().collect(); + state_list.sort_by_key(|s| s.resources); + println!("### states = {:#?}", state_list); + println!("### expected_state = {:#?}", expected_state); + panic!(); + } + let new_states: StateSet = states + .iter() + .flat_map(|state| state.step(bp0, time, TIME_LIMIT)) + .collect(); + states = new_states; + } + + let mut state_list: Vec<_> = states.into_iter().collect(); + + state_list.sort_by_key(|s| s.resources); + state_list.reverse(); + + println!("states = {:#?}", &state_list[..4.min(state_list.len())]); + } +}