Skip to content

Commit d6dffcf

Browse files
committed
adventofcode/cc: add solution for 2022/19.
I _hated_ this problem when I first encountered it. It made me put it away for a long while. Then someone at work linked me a _fantastic_ video with some great ideas for optimization: https://www.youtube.com/watch?v=5rb0vvJ7NCY So the ideas encoded in this commit are not mine; all credit goes to the video author. However, I can't seem to reach the very fast runtimes he achieves. I suspect differences in programming languages, compilers, and computer components are at play here. Benchmarks from `./bazel run -c opt //adventofcode/cc/year2022:day19_benchmark`: 2023-06-18T16:45:52+01:00 Running /home/saser/.cache/bazel/_bazel_saser/06ad534583e887506ca6aa175f8ed7b5/execroot/code/bazel-out/k8-opt/bin/adventofcode/cc/year2022/day19_benchmark Run on (16 X 4679.3 MHz CPU s) CPU Caches: L1 Data 32 KiB (x8) L1 Instruction 32 KiB (x8) L2 Unified 512 KiB (x8) L3 Unified 16384 KiB (x1) Load Average: 0.81, 0.96, 0.93 ***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead. ----------------------------------------------------------------- Benchmark Time CPU Iterations ----------------------------------------------------------------- BM_Part1/day19.real.in 129399485 ns 129359114 ns 5 BM_Part2/day19.real.in 371451771 ns 371345686 ns 2
1 parent 3c0ed7e commit d6dffcf

File tree

8 files changed

+353
-0
lines changed

8 files changed

+353
-0
lines changed

adventofcode/cc/year2022/BUILD.bazel

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,44 @@ cc_aoc_benchmark(
707707
},
708708
)
709709

710+
cc_library(
711+
name = "day19",
712+
srcs = ["day19.cc"],
713+
hdrs = ["day19.h"],
714+
deps = [
715+
"@com_google_absl//absl/container:flat_hash_map",
716+
"@com_google_absl//absl/log:check",
717+
"@com_google_absl//absl/status",
718+
"@com_google_absl//absl/status:statusor",
719+
"@com_google_absl//absl/strings",
720+
"@com_googlesource_code_re2//:re2",
721+
],
722+
)
723+
724+
cc_aoc_test(
725+
name = "day19_test",
726+
library = ":day19",
727+
part1 = {
728+
"//adventofcode/data/year2022:day19.example.in": "//adventofcode/data/year2022:day19.example.part1.out",
729+
"//adventofcode/data/year2022:day19.real.in": "//adventofcode/data/year2022:day19.real.part1.out",
730+
},
731+
part2 = {
732+
"//adventofcode/data/year2022:day19.example.in": "//adventofcode/data/year2022:day19.example.part2.out",
733+
"//adventofcode/data/year2022:day19.real.in": "//adventofcode/data/year2022:day19.real.part2.out",
734+
},
735+
)
736+
737+
cc_aoc_benchmark(
738+
name = "day19_benchmark",
739+
library = ":day19",
740+
part1 = {
741+
"//adventofcode/data/year2022:day19.real.in": "//adventofcode/data/year2022:day19.real.part1.out",
742+
},
743+
part2 = {
744+
"//adventofcode/data/year2022:day19.real.in": "//adventofcode/data/year2022:day19.real.part2.out",
745+
},
746+
)
747+
710748
cc_library(
711749
name = "day20",
712750
srcs = ["day20.cc"],

adventofcode/cc/year2022/day19.cc

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
#include "adventofcode/cc/year2022/day19.h"
2+
3+
#include <algorithm>
4+
#include <cstdint>
5+
#include <string>
6+
#include <utility>
7+
8+
#include "absl/container/flat_hash_map.h"
9+
#include "absl/log/check.h"
10+
#include "absl/status/status.h"
11+
#include "absl/status/statusor.h"
12+
#include "absl/strings/str_split.h"
13+
#include "absl/strings/string_view.h"
14+
#include "re2/re2.h"
15+
16+
namespace adventofcode {
17+
namespace cc {
18+
namespace year2022 {
19+
namespace day19 {
20+
namespace {
21+
22+
// The various optimizations in this solution are ones I didn't come up with
23+
// myself. Instead, I got them from this _fantastic_ video:
24+
// https://www.youtube.com/watch?v=5rb0vvJ7NCY. All credit to that author -- I
25+
// just internalized his ideas and translated them into code here.
26+
27+
struct Blueprint {
28+
uint8_t id;
29+
uint8_t ore; // Unit: ore.
30+
uint8_t clay; // Unit: ore.
31+
std::pair<uint8_t, uint8_t> obsidian; // Units: <ore, clay>.
32+
std::pair<uint8_t, uint8_t> geode; // Units: <ore, obsidian>.
33+
34+
uint8_t max_ore_cost;
35+
uint8_t max_clay_cost;
36+
uint8_t max_obsidian_cost;
37+
38+
static Blueprint Parse(absl::string_view line) {
39+
// For some reason that I haven't cared to debug, RE2 can't FullMatch into
40+
// the uint8_t values in Blueprint. So we set up corresponding int
41+
// variables here, parse into them, then feed them to the Blueprint below.
42+
int id;
43+
int ore;
44+
int clay;
45+
std::pair<int, int> obsidian;
46+
std::pair<int, int> geode;
47+
static RE2 blueprint_regex(
48+
R"(Blueprint (\d+): Each ore robot costs (\d+) ore. Each clay robot costs (\d+) ore. Each obsidian robot costs (\d+) ore and (\d+) clay. Each geode robot costs (\d+) ore and (\d+) obsidian.)");
49+
CHECK(RE2::FullMatch(line, blueprint_regex, &id, &ore, &clay,
50+
&obsidian.first, &obsidian.second, &geode.first,
51+
&geode.second))
52+
<< line;
53+
Blueprint b{
54+
.id = uint8_t(id),
55+
.ore = uint8_t(ore),
56+
.clay = uint8_t(clay),
57+
.obsidian = std::make_pair(obsidian.first, obsidian.second),
58+
.geode = std::make_pair(geode.first, geode.second),
59+
};
60+
b.max_ore_cost = std::max({b.ore, b.clay, b.obsidian.first, b.geode.first});
61+
b.max_clay_cost = b.obsidian.second;
62+
b.max_obsidian_cost = b.geode.second;
63+
return b;
64+
}
65+
};
66+
67+
struct State {
68+
// How many minutes have passed.
69+
uint8_t minutes;
70+
71+
// How many robots of each kind we have.
72+
uint8_t ore_robots;
73+
uint8_t clay_robots;
74+
uint8_t obsidian_robots;
75+
uint8_t geode_robots;
76+
77+
// How much resources we have.
78+
uint8_t ore;
79+
uint8_t clay;
80+
uint8_t obsidian;
81+
uint8_t geodes;
82+
83+
inline bool CanBuildOreRobot(const Blueprint& b) const {
84+
return ore >= b.ore;
85+
}
86+
87+
inline bool CanBuildClayRobot(const Blueprint& b) const {
88+
return ore >= b.clay;
89+
}
90+
91+
inline bool CanBuildObsidianRobot(const Blueprint& b) const {
92+
return ore >= b.obsidian.first && clay >= b.obsidian.second;
93+
}
94+
95+
inline bool CanBuildGeodeRobot(const Blueprint& b) const {
96+
return ore >= b.geode.first && obsidian >= b.geode.second;
97+
}
98+
99+
inline State BuildOreRobot(const Blueprint& b) const {
100+
State s = *this;
101+
s.ore -= b.ore;
102+
s.ore_robots++;
103+
return s;
104+
}
105+
106+
inline State BuildClayRobot(const Blueprint& b) const {
107+
State s = *this;
108+
s.ore -= b.clay;
109+
s.clay_robots++;
110+
return s;
111+
}
112+
113+
inline State BuildObsidianRobot(const Blueprint& b) const {
114+
State s = *this;
115+
s.ore -= b.obsidian.first;
116+
s.clay -= b.obsidian.second;
117+
s.obsidian_robots++;
118+
return s;
119+
}
120+
121+
inline State BuildGeodeRobot(const Blueprint& b) const {
122+
State s = *this;
123+
s.ore -= b.geode.first;
124+
s.obsidian -= b.geode.second;
125+
s.geode_robots++;
126+
return s;
127+
}
128+
129+
inline State Step() const {
130+
State s = *this;
131+
s.minutes++;
132+
s.ore += ore_robots;
133+
s.clay += clay_robots;
134+
s.obsidian += obsidian_robots;
135+
s.geodes += geode_robots;
136+
return s;
137+
}
138+
139+
// CannotBeat determines whether this state cannot possibly result in more
140+
// geodes being produced than `max`. This is useful as an optimization: if
141+
// CannotBeat returns true, then there's no point in continuing to explore
142+
// this State.
143+
inline bool CannotBeat(uint8_t limit, uint8_t max) const {
144+
// While we don't know exactly how many geodes this state can produce at
145+
// best, we can compute an upper bound. That upper bound is the sum of:
146+
//
147+
// 1. The current number of geodes. This is simple: it's stored in the pack.
148+
//
149+
// 2. The current number of geode robots. Each robot will produce 1 geode
150+
// each remaining minute, so we take the number of remaining minutes
151+
// multiplied by the current number of geode robots.
152+
//
153+
// 3. A best-case scenario of future geode robots: we build a new geode
154+
// robot each minute. There's no guarantee we would be able to do that, but
155+
// we absolutely cannot do _better_ than that. This becomes an arithmetic
156+
// sum: if we for e.g. the next 3 minutes build 1 geode robot each minute,
157+
// they will produce 0 + 1 + 2 geodes, and so on. Note the off-by-one
158+
// situation here: if we have N remaining minutes, the robots will only
159+
// actually produce geodes for N-1 minutes. So we take the arithmetic sum
160+
// from 1 to N-1, which is (N*(N-1))/2.
161+
162+
uint8_t remaining = limit - minutes;
163+
164+
uint16_t upper_bound = geodes // #1
165+
+ geode_robots * remaining // #2
166+
+ remaining * (remaining - 1) / 2; // #3
167+
return upper_bound <= max;
168+
}
169+
};
170+
171+
void MaxGeodes(
172+
State s, const Blueprint& b,
173+
// The maximum number of minutes.
174+
uint8_t limit,
175+
// The highest result we've seen so far.
176+
uint8_t& max,
177+
// Whether we are "allowed" to build one
178+
// of these robots this step. These can be false if this state was reached
179+
// by waiting, and we could have built a robot instead of waiting.
180+
bool allowed_ore, bool allowed_clay, bool allowed_obsidian) {
181+
if (s.minutes == limit) {
182+
max = std::max(max, s.geodes);
183+
return;
184+
}
185+
if (s.CannotBeat(limit, max)) {
186+
return;
187+
}
188+
State next = s.Step();
189+
if (s.CanBuildGeodeRobot(b)) {
190+
MaxGeodes(next.BuildGeodeRobot(b), b, limit, max, true, true, true);
191+
// Building a geode robot is the best thing we can do -- there's no reason
192+
// to explore other states.
193+
return;
194+
}
195+
bool new_allowed_ore = true;
196+
bool new_allowed_clay = true;
197+
bool new_allowed_obsidian = true;
198+
if (
199+
// Whether we are allowed to build after waiting.
200+
allowed_obsidian
201+
// No point in building more obsidian robots if we are already producing
202+
// enough each minute to build geode robots.
203+
&& s.obsidian_robots < b.max_obsidian_cost
204+
// Do we even have the resources?
205+
&& s.CanBuildObsidianRobot(b)) {
206+
new_allowed_obsidian = false;
207+
MaxGeodes(next.BuildObsidianRobot(b), b, limit, max, true, true, true);
208+
}
209+
if (
210+
// Whether we are allowed to build after waiting.
211+
allowed_clay
212+
// No point in building more obsidian robots if we are already producing
213+
// enough each minute to build obsidian robots.
214+
&& s.clay_robots < b.max_clay_cost
215+
// Whether we have the resources.
216+
&& s.CanBuildClayRobot(b)) {
217+
new_allowed_clay = false;
218+
MaxGeodes(next.BuildClayRobot(b), b, limit, max, true, true, true);
219+
}
220+
if (
221+
// Whether we are allowed to build after waiting.
222+
allowed_ore
223+
// No point in building more ore robots if we are already producing
224+
// enough each minute to build any other robot.
225+
&& s.ore_robots < b.max_ore_cost
226+
// Whether we have the resources
227+
&& s.CanBuildOreRobot(b)) {
228+
new_allowed_ore = false;
229+
MaxGeodes(next.BuildOreRobot(b), b, limit, max, true, true, true);
230+
}
231+
MaxGeodes(next, b, limit, max, new_allowed_ore, new_allowed_clay,
232+
new_allowed_obsidian);
233+
}
234+
235+
uint8_t MaxGeodes(const Blueprint& b, uint8_t limit) {
236+
State s{};
237+
s.ore_robots = 1;
238+
uint8_t max = 0;
239+
MaxGeodes(s, b, limit, max, true, true, true);
240+
return max;
241+
}
242+
243+
absl::StatusOr<std::string> solve(absl::string_view input, bool part1) {
244+
std::vector<Blueprint> blueprints;
245+
for (absl::string_view line :
246+
absl::StrSplit(input, '\n', absl::SkipEmpty())) {
247+
blueprints.push_back(Blueprint::Parse(line));
248+
}
249+
if (part1) {
250+
uint8_t limit = 24;
251+
uint16_t answer = 0;
252+
for (const Blueprint& b : blueprints) {
253+
answer += b.id * MaxGeodes(b, limit);
254+
}
255+
return std::to_string(answer);
256+
} else {
257+
uint8_t limit = 32;
258+
uint16_t answer = 1;
259+
for (auto it = blueprints.cbegin();
260+
it != blueprints.cend() && it != blueprints.cbegin() + 3; it++) {
261+
answer *= MaxGeodes(*it, limit);
262+
}
263+
return std::to_string(answer);
264+
}
265+
}
266+
267+
} // namespace
268+
269+
absl::StatusOr<std::string> Part1(absl::string_view input) {
270+
return solve(input, /*part1=*/true);
271+
}
272+
273+
absl::StatusOr<std::string> Part2(absl::string_view input) {
274+
return solve(input, /*part1=*/false);
275+
}
276+
} // namespace day19
277+
} // namespace year2022
278+
} // namespace cc
279+
} // namespace adventofcode
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
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.
2+
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.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
33
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3472
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Blueprint 1: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 20 clay. Each geode robot costs 3 ore and 18 obsidian.
2+
Blueprint 2: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 16 clay. Each geode robot costs 3 ore and 9 obsidian.
3+
Blueprint 3: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 4 ore and 8 clay. Each geode robot costs 2 ore and 8 obsidian.
4+
Blueprint 4: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 18 clay. Each geode robot costs 4 ore and 16 obsidian.
5+
Blueprint 5: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 19 clay. Each geode robot costs 4 ore and 15 obsidian.
6+
Blueprint 6: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 8 clay. Each geode robot costs 4 ore and 14 obsidian.
7+
Blueprint 7: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 11 clay. Each geode robot costs 3 ore and 8 obsidian.
8+
Blueprint 8: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 7 clay. Each geode robot costs 3 ore and 10 obsidian.
9+
Blueprint 9: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 15 clay. Each geode robot costs 2 ore and 8 obsidian.
10+
Blueprint 10: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 20 clay. Each geode robot costs 2 ore and 17 obsidian.
11+
Blueprint 11: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 16 clay. Each geode robot costs 2 ore and 9 obsidian.
12+
Blueprint 12: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 19 clay. Each geode robot costs 4 ore and 13 obsidian.
13+
Blueprint 13: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 17 clay. Each geode robot costs 4 ore and 20 obsidian.
14+
Blueprint 14: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 17 clay. Each geode robot costs 3 ore and 19 obsidian.
15+
Blueprint 15: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 4 ore and 17 obsidian.
16+
Blueprint 16: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 9 clay. Each geode robot costs 3 ore and 9 obsidian.
17+
Blueprint 17: Each ore robot costs 2 ore. Each clay robot costs 4 ore. Each obsidian robot costs 3 ore and 20 clay. Each geode robot costs 2 ore and 17 obsidian.
18+
Blueprint 18: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 17 clay. Each geode robot costs 4 ore and 16 obsidian.
19+
Blueprint 19: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 4 ore and 5 clay. Each geode robot costs 3 ore and 10 obsidian.
20+
Blueprint 20: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 7 clay. Each geode robot costs 4 ore and 13 obsidian.
21+
Blueprint 21: Each ore robot costs 3 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 20 clay. Each geode robot costs 4 ore and 7 obsidian.
22+
Blueprint 22: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 14 clay. Each geode robot costs 3 ore and 8 obsidian.
23+
Blueprint 23: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 19 clay. Each geode robot costs 3 ore and 13 obsidian.
24+
Blueprint 24: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 15 clay. Each geode robot costs 4 ore and 20 obsidian.
25+
Blueprint 25: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 14 clay. Each geode robot costs 4 ore and 15 obsidian.
26+
Blueprint 26: Each ore robot costs 4 ore. Each clay robot costs 3 ore. Each obsidian robot costs 4 ore and 8 clay. Each geode robot costs 3 ore and 7 obsidian.
27+
Blueprint 27: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 4 ore and 19 clay. Each geode robot costs 4 ore and 7 obsidian.
28+
Blueprint 28: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 2 ore and 11 clay. Each geode robot costs 4 ore and 8 obsidian.
29+
Blueprint 29: Each ore robot costs 3 ore. Each clay robot costs 3 ore. Each obsidian robot costs 2 ore and 15 clay. Each geode robot costs 3 ore and 9 obsidian.
30+
Blueprint 30: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 17 clay. Each geode robot costs 2 ore and 13 obsidian.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1349
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
21840

0 commit comments

Comments
 (0)