Skip to content

Commit c454b5c

Browse files
committed
Document Year 2022 Day 19
1 parent f976592 commit c454b5c

File tree

1 file changed

+57
-0
lines changed

1 file changed

+57
-0
lines changed

src/year2022/day19.rs

+57
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,49 @@
1+
//! # Not Enough Minerals
2+
//!
3+
//! The solution is [branch and bound](https://en.wikipedia.org/wiki/Branch_and_bound) using
4+
//! a [depth first search](https://en.wikipedia.org/wiki/Depth-first_search) to enumerate every
5+
//! possible combination combined with heuristics to prune those combinations in order to achieve
6+
//! a reasonable running time.
7+
//!
8+
//! The most import heuristic is:
9+
//! * Assume ore and clay are infinite.
10+
//! * Check if we can do better than the highest score so far in the remaining time, building
11+
//! only geode or obsidian bots.
12+
//!
13+
//! As these simplified rules will always score higher than the real rules, we can immediately
14+
//! prune any branch that can't possibly exceed the current high score.
15+
//!
16+
//! The second helpful heuristic is:
17+
//! * Don't build more bots for a particular mineral than the maximum possible consumption in
18+
//! a single turn.
19+
//!
20+
//! As we can only build one bot per turn, we will never need to generate more resources than
21+
//! that bot can use. For example, if ore robots need 2 ore, clay robots 3 ore, obsidian robots
22+
//! 4 ore and geode robots 5 ore, then the most possible ore robots that we need to build is 5.
23+
//! Any more would go to waste. The same applies for clay and obsidian.
24+
//!
25+
//! The third helpful heuristic is:
26+
//! * Don't build any robot during the last minute
27+
//! * Don't build ore or obsidian robots during the last 3 minutes.
28+
//! * Don't build clay robots during the last 5 minutes.
29+
//!
30+
//! Building any robot during the last minute means that it will be ready *after* the time runs
31+
//! out so it will contribute nothing. The other two rules are corollaries of this rule.
32+
//!
33+
//! For example say we build an obsidian robot with 3 minutes left. It will be ready and collect
34+
//! a resource with two minutes left, which can be spent on a geode robot with 1 minute left,
35+
//! which is too late.
36+
//!
37+
//! Since we only need clay for obsidian robots it doesn't make sense to build clay robots less
38+
//! than two minutes before the cutoff for obsidian robots.
39+
//!
40+
//! The final important optimization is that we don't increment minute by minute. Instead once
41+
//! we decide to buld a robot of a particular type, we "fast forward" in time until there are
42+
//! enough resources to build that robot. This cuts down on a lot of duplicate intermediate states.
143
use crate::util::parse::*;
244
use std::ops::{Add, Sub};
345

46+
/// Each robot generates 1 mineral of a particular type.
447
const ZERO: Mineral = Mineral::from(0, 0, 0, 0);
548
const ORE_BOT: Mineral = Mineral::from(1, 0, 0, 0);
649
const CLAY_BOT: Mineral = Mineral::from(0, 1, 0, 0);
@@ -20,11 +63,13 @@ impl Mineral {
2063
Mineral { ore, clay, obsidian, geode }
2164
}
2265

66+
/// This is used to compare robot costs so we don't need to check geodes.
2367
fn less_than_equal(self, rhs: Self) -> bool {
2468
self.ore <= rhs.ore && self.clay <= rhs.clay && self.obsidian <= rhs.obsidian
2569
}
2670
}
2771

72+
/// Implement operators so that we can use `+` and `-` notation to add and subtract minerals.
2873
impl Add for Mineral {
2974
type Output = Self;
3075

@@ -101,9 +146,12 @@ fn maximize(blueprint: &Blueprint, time: u32) -> u32 {
101146
result
102147
}
103148

149+
/// Depth first search over every possible combination pruning branches using heuristics.
104150
fn dfs(blueprint: &Blueprint, result: &mut u32, time: u32, bots: Mineral, resources: Mineral) {
151+
// Extrapolate total geodes from the current state in the remaining time.
105152
*result = (*result).max(resources.geode + bots.geode * time);
106153

154+
// Check if this state can improve on the existing high score.
107155
if heuristic(blueprint, *result, time, bots, resources) {
108156
if bots.obsidian > 0 && time > 1 {
109157
next(blueprint, result, time, bots, resources, GEODE_BOT, blueprint.geode_cost);
@@ -120,6 +168,11 @@ fn dfs(blueprint: &Blueprint, result: &mut u32, time: u32, bots: Mineral, resour
120168
}
121169
}
122170

171+
/// Simplify the blueprints so that we only need to build either geode or obsidian robots,
172+
/// then check that the estimated maximum possible score is greater than the current high score.
173+
///
174+
/// Since ore and clay are infinite this will always score higher, so we can immediately
175+
/// prune any branch that can't possibly beat the high score.
123176
#[inline]
124177
fn heuristic(
125178
blueprint: &Blueprint,
@@ -129,9 +182,11 @@ fn heuristic(
129182
mut resources: Mineral,
130183
) -> bool {
131184
for _ in 0..time {
185+
// Assume ore and clay are infinite.
132186
resources.ore = blueprint.max_ore;
133187
resources.clay = blueprint.max_clay;
134188

189+
// Only attempt to build geode or obsidian robots.
135190
if blueprint.geode_cost.less_than_equal(resources) {
136191
resources = resources + bots - blueprint.geode_cost;
137192
bots = bots + GEODE_BOT;
@@ -144,6 +199,8 @@ fn heuristic(
144199
resources.geode > result
145200
}
146201

202+
/// "Fast forward" in time until we can build a robot of a particular type. This could possibly
203+
/// by the next minute if we already have enough resources.
147204
#[inline]
148205
fn next(
149206
blueprint: &Blueprint,

0 commit comments

Comments
 (0)