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.
1
43
use crate :: util:: parse:: * ;
2
44
use std:: ops:: { Add , Sub } ;
3
45
46
+ /// Each robot generates 1 mineral of a particular type.
4
47
const ZERO : Mineral = Mineral :: from ( 0 , 0 , 0 , 0 ) ;
5
48
const ORE_BOT : Mineral = Mineral :: from ( 1 , 0 , 0 , 0 ) ;
6
49
const CLAY_BOT : Mineral = Mineral :: from ( 0 , 1 , 0 , 0 ) ;
@@ -20,11 +63,13 @@ impl Mineral {
20
63
Mineral { ore, clay, obsidian, geode }
21
64
}
22
65
66
+ /// This is used to compare robot costs so we don't need to check geodes.
23
67
fn less_than_equal ( self , rhs : Self ) -> bool {
24
68
self . ore <= rhs. ore && self . clay <= rhs. clay && self . obsidian <= rhs. obsidian
25
69
}
26
70
}
27
71
72
+ /// Implement operators so that we can use `+` and `-` notation to add and subtract minerals.
28
73
impl Add for Mineral {
29
74
type Output = Self ;
30
75
@@ -101,9 +146,12 @@ fn maximize(blueprint: &Blueprint, time: u32) -> u32 {
101
146
result
102
147
}
103
148
149
+ /// Depth first search over every possible combination pruning branches using heuristics.
104
150
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.
105
152
* result = ( * result) . max ( resources. geode + bots. geode * time) ;
106
153
154
+ // Check if this state can improve on the existing high score.
107
155
if heuristic ( blueprint, * result, time, bots, resources) {
108
156
if bots. obsidian > 0 && time > 1 {
109
157
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
120
168
}
121
169
}
122
170
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.
123
176
#[ inline]
124
177
fn heuristic (
125
178
blueprint : & Blueprint ,
@@ -129,9 +182,11 @@ fn heuristic(
129
182
mut resources : Mineral ,
130
183
) -> bool {
131
184
for _ in 0 ..time {
185
+ // Assume ore and clay are infinite.
132
186
resources. ore = blueprint. max_ore ;
133
187
resources. clay = blueprint. max_clay ;
134
188
189
+ // Only attempt to build geode or obsidian robots.
135
190
if blueprint. geode_cost . less_than_equal ( resources) {
136
191
resources = resources + bots - blueprint. geode_cost ;
137
192
bots = bots + GEODE_BOT ;
@@ -144,6 +199,8 @@ fn heuristic(
144
199
resources. geode > result
145
200
}
146
201
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.
147
204
#[ inline]
148
205
fn next (
149
206
blueprint : & Blueprint ,
0 commit comments