From 19d5613bd9edc7e80ad586f9206ecb8b046a85b0 Mon Sep 17 00:00:00 2001 From: Tobias Marschner Date: Wed, 10 Apr 2024 10:51:19 +0200 Subject: [PATCH] Combined solution for parts 1+2 of 2022/day19, both finishing in <2 sec --- 2022/{day19-part1 => day19}/Cargo.toml | 2 +- 2022/{day19-part1 => day19}/src/main.rs | 156 ++++++++++++++++++++---- 2 files changed, 133 insertions(+), 25 deletions(-) rename 2022/{day19-part1 => day19}/Cargo.toml (88%) rename 2022/{day19-part1 => day19}/src/main.rs (62%) diff --git a/2022/day19-part1/Cargo.toml b/2022/day19/Cargo.toml similarity index 88% rename from 2022/day19-part1/Cargo.toml rename to 2022/day19/Cargo.toml index 116da57..58edd67 100644 --- a/2022/day19-part1/Cargo.toml +++ b/2022/day19/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "day19-part1" +name = "day19" version = "0.1.0" edition = "2021" diff --git a/2022/day19-part1/src/main.rs b/2022/day19/src/main.rs similarity index 62% rename from 2022/day19-part1/src/main.rs rename to 2022/day19/src/main.rs index cdea3b0..c0cc453 100644 --- a/2022/day19-part1/src/main.rs +++ b/2022/day19/src/main.rs @@ -13,11 +13,9 @@ struct Blueprint { optimal_geode_count: u16, } -const TOTAL_RUNTIME: usize = 24; - impl Blueprint { // Solve the given blueprint using BFS. - fn solve_bfs(&mut self) { + fn solve_bfs(&mut self, total_runtime: u16) { // For performance reasons we will search the solution space using breadth-first search. // vec_a has the RecursionStates for the current timeslot, while vec_b has the next slot's states. @@ -39,8 +37,21 @@ impl Blueprint { // Iterate over all timeslots. // Building a robot at t=1 cannot influence the final geode-count, // so it's omitted from the simulation here. - for ts in (2usize..=TOTAL_RUNTIME).rev() { - // println!("Now at {} remaining time. Processing {} input RSs ...", ts, next_hs.len()); + let mut early_exit = false; + for ts in (2u16..=total_runtime).rev() { + + // Have we reached >= 2^20 elements on the input? Time to go for DFS instead. + // Additionally, the queue-overhead shouldn't be worth it for the last few timesteps. + if vec_a.len() >= 2u64.pow(20) as usize || ts <= 3 { + // println!("Switching to recursive solving ..."); + // Iterate over all possibilities and run recursively. + for rs in &vec_a { + self.solve_recursive(*rs, ts); + } + // And now we're done proper, no need to run the remaining loop iterations. + early_exit = true; + break; + } // Process every RS of the past timeslot // to find all the states for the current timeslot. @@ -104,32 +115,111 @@ impl Blueprint { } // Done! - println!("Finished simulation round for t = {}", ts); - println!(" inserted elements: {}", vec_b.len()); + // println!("Finished simulation round for t = {}", ts); + // println!(" inserted elements: {}", vec_b.len()); // Prune elements. prune_states(&mut vec_b, &mut vec_a); - println!(" elements after prune: {}", vec_a.len()); + // println!(" elements after prune: {}", vec_a.len()); // Clear vec_b since all the relevant states have been copied over to vec_a. vec_b.clear(); - - // for e in &vec_a { - // e.print(); - // println!(); - // } - // println!(); - // - // if ts == 15 { - // std::process::exit(1); - // } } // Collect and print the final geode count. // Remember that we still have to simulate the geode-collection for t=1, // hence `e.geode + e.geode_robots as u16`. - self.optimal_geode_count = vec_a.iter().map(|e| e.geode + e.geode_robots as u16).max().unwrap(); - println!("Found optimal geode count: {}", self.optimal_geode_count); + if !early_exit { + self.optimal_geode_count = vec_a + .iter() + .map(|e| e.geode + e.geode_robots as u16) + .max() + .unwrap(); + } + // println!("Found optimal geode count: {}", self.optimal_geode_count); + } + + // Solve the task recursively, providing the current state and remaining time. + // Essentially, and in contrast to `solve_bfs`, this recursive solver performs + // depth-first-search (DFS) on the solution space instead of BFS. + // This removes our ability to prune redundant elements, but doesn't require + // keeping a queue of elements, making for a *much* lighter memory footprint. + // Recommended for the final few timesteps. + fn solve_recursive(&mut self, rs: RecursionState, t: u16) { + // print!("t = {}, ", t); + // rs.print(); + // println!(); + // Exit condition. If t == 1, we're basically done. + // No need to build the final robot, it can't influence the final geode result. + // Simply add one more round of harvesting (rs.geode_robots) and check for improvements. + if t == 1 { + let next_geode_count = rs.geode + rs.geode_robots as u16; + if next_geode_count > self.optimal_geode_count { + // Update the optimal result, if improved. + self.optimal_geode_count = next_geode_count; + } + return; + } + + // Check a cutoff-condition, in case this branch is not worth it. + let upper_bound = rs.geode // The resources we already have. + // The resource the already existing robots would produce. + + rs.geode_robots as u16 * t + // The resources we would get if we produced one robot every timeslot. + // This is the triangular number for (t - 1). + + (t - 1) * t / 2; + // Now check if this would be an improvement. + if upper_bound <= self.optimal_geode_count { + // No point continuing. + return; + } + + // The following section is basically the same as in `solve_bfs`. + + // Copy over the current state and let time for it pass. + // This is the same no matter what type of robot we build since the robot will + // go live at the end of the timeslot, not at its beginning. + let mut next_rs = rs; + next_rs.ore += next_rs.ore_robots as u16; + next_rs.clay += next_rs.clay_robots as u16; + next_rs.obsidian += next_rs.obsidian_robots as u16; + next_rs.geode += next_rs.geode_robots as u16; + + // Check whether we can build the different robots, using `rs` and not `next_rs` + // since the resources have to be allocated at the beginning of the turn. + + // (1) Ore Robot + if rs.ore >= self.ore_robot_ore_cost { + let mut nrs = next_rs; + nrs.ore -= self.ore_robot_ore_cost; + nrs.ore_robots += 1; + self.solve_recursive(nrs, t - 1); + } + // (2) Clay Robot + if rs.ore >= self.clay_robot_ore_cost { + let mut nrs = next_rs; + nrs.ore -= self.clay_robot_ore_cost; + nrs.clay_robots += 1; + self.solve_recursive(nrs, t - 1); + } + // (3) Obsidian Robot + if rs.ore >= self.obsidian_robot_ore_cost && rs.clay >= self.obsidian_robot_clay_cost { + let mut nrs = next_rs; + nrs.ore -= self.obsidian_robot_ore_cost; + nrs.clay -= self.obsidian_robot_clay_cost; + nrs.obsidian_robots += 1; + self.solve_recursive(nrs, t - 1); + } + // (4) Geode Robot + if rs.ore >= self.geode_robot_ore_cost && rs.obsidian >= self.geode_robot_obsidian_cost { + let mut nrs = next_rs; + nrs.ore -= self.geode_robot_ore_cost; + nrs.obsidian -= self.geode_robot_obsidian_cost; + nrs.geode_robots += 1; + self.solve_recursive(nrs, t - 1); + } + // (5) Build nothing and let time pass. + self.solve_recursive(next_rs, t - 1); } } @@ -232,18 +322,36 @@ fn main() { }) .collect::>(); - // Solve for every blueprint. + // PART ONE + + // Solve for every blueprint with time 24. for bp in &mut blueprints { // Solve every blueprint with TOTAL_RUNTIME minutes of time. - println!("Solving Blueprint {}", bp.id); - bp.solve_bfs(); + // println!("Solving Blueprint {}", bp.id); + bp.solve_bfs(24u16); } println!( - "Total Quality Level: {}", + "Total Quality Level for Part 1: {}", blueprints .iter() .map(|b| b.id * b.optimal_geode_count) .sum::() ); + + // PART TWO + + // Now solve the first three blueprints again, but for 32 minutes. + for bp in blueprints.iter_mut().take(3) { + bp.solve_bfs(32u16); + } + + println!( + "Multiplied Geode Counts for Part 2: {}", + blueprints + .iter() + .take(3) + .map(|b| b.optimal_geode_count as u64) + .product::() + ); }