Advent Of Code 2022 第十九天 - 不够矿石

本文是关于 Advent Of Code 2022 的。
编程圣诞日历,圣诞节前每天一个编程问题。

有兴趣的朋友可以在官网答题,用自己喜欢的编程方式去收集星星⭐️。在官网答题需要登陆并下载为你单独生成的谜题输入,每个人的都不一样。

问题

优化你的机器人工厂,使它能够生产更多的机器人。

第一部分

你的扫描结果显示,熔岩确实形成了黑曜石!

风向已经改变,不再向你传送熔岩液滴,所以你和大象们离开了洞穴。当你这样做的时候,你注意到池塘周围有一系列的晶洞。也许你可以用黑曜石制造一些 晶洞破开机器人 ,然后把它们破开?

为了收集池塘底部的黑曜石,你需要防水的 黑曜石收集机器人 。幸运的是,附近有大量的粘土,你可以用它来使它们防水。

为了收获粘土,你需要特殊用途的 粘土收集机器人 。要制造任何类型的机器人,你都需要 矿石 ,它也很丰富,但与粘土的方向相反。

收集矿石需要有大钻头的 矿石收集机器人 。幸运的是, 你的背包里正好有一个收集矿石的机器人 ,你可以用它来启动整个行动。

每个机器人每分钟可以收集1种资源类型。机器人工厂(也是从你的背包里拿出来的)建造任何类型的机器人也需要1分钟,尽管它在开始建造时要消耗必要的资源。

机器人工厂有许多 蓝图 (你的谜题输入)可以选择,但一旦你用蓝图配置了它,你就不能改变它。你需要研究出哪种蓝图是最好的。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
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.

(为了便于辨认,这里的蓝图都有换行。机器人工厂的实际蓝图种类是每行提供一个蓝图)。

大象开始看起来很饿了,所以你不应该花太长时间;你需要通过计算蓝图能在 24分钟 后最大打开晶洞的数量,即确定建造哪些机器人以及何时建造它们。

在上面的例子中使用蓝图1,你能在24分钟内打开的最大数量的晶洞是 9

然而,通过使用上述例子中的蓝图2,你可以做得更好:你在24分钟内可以打开晶洞的最大数量是 12

通过 将该蓝图的ID号 与使用该蓝图在 24分钟内可打开晶洞的最大数量 相乘来确定每个蓝图的 质量等级 。在这个例子中,第一个蓝图的ID是1,可以打开9个晶洞,所以它的质量等级是 9 。第二个蓝图的ID是2,可以打开12个晶洞,所以它的质量等级是 24 。最后,如果你把列表中所有蓝图的质量等级 加起来 ,你会得到 33

用它在24分钟内能生产的最大数量的晶洞来确定每个蓝图的质量等级。 如果你把列表中所有蓝图的质量水平加起来,你会得到什么?

第二部分

在你选择最佳蓝图的时候,大象自己找到了一些食物,所以你没有那么着急;你想,在风向再次改变之前,你大概有 32分钟 的时间,你需要离开火山爆发的范围。

不幸的是,其中一只大象 吃掉了你的大部分蓝图清单 !现在,你的清单中只有前三张蓝图是完整的。

在32分钟内,蓝图1(来自上面的例子)能打开的最大数量的晶洞是 56

然而,上面例子中的蓝图2仍然更好;使用它,你在32分钟内能打开的最大数量的晶洞是 62

不再有足够的蓝图来担心质量水平 。相反,对于前三个蓝图中的每一个,确定你能打开的最大数量的晶洞;然后,将这三个值相乘。

不要担心质量水平;相反,只要确定你能用前三个蓝图中的每一个打开的最大数量的晶洞。 如果你把这些数字相乘,你会得到什么?

代码

完整的代码可以在 这里 找到。

19.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
use std::cmp::max;
use itertools::Itertools;
use rayon::prelude::*;
use regex::Regex;
use crate::Material::{Clay, Geode, Obsidian, Ore};

pub type RecipePart = (u32, Material);

#[derive(Copy, Clone, Debug)]
pub enum Material {
Ore = 0,
Clay = 1,
Obsidian = 2,
Geode = 3,
}

#[derive(Clone, Debug)]
pub struct Blueprint {
id: u32,
robot_recipes: [Vec<RecipePart>; 4],
}

#[derive(Copy, Clone)]
pub struct SearchState {
time_remaining: u32,
robots: [u32; 4],
materials: [u32; 4],
}

impl SearchState {
fn can_build_robot(&self, robot_type: usize, blueprint: &Blueprint, max_materials: &[u32]) -> bool {
let recipe = &blueprint.robot_recipes[robot_type];
let maxed_out = self.robots[robot_type] >= max_materials[robot_type];
!maxed_out && recipe.iter().all(|&(amount, material)| self.materials[material as usize] >= amount)
}

fn build_robot(&mut self, robot_type: usize, blueprint: &Blueprint) {
self.robots[robot_type] += 1;
for &(amount, material) in &blueprint.robot_recipes[robot_type] {
self.materials[material as usize] -= amount;
}
}

fn un_build_robot(&mut self, robot_type: usize, blueprint: &Blueprint) {
self.robots[robot_type] -= 1;
for &(amount, material) in &blueprint.robot_recipes[robot_type] {
self.materials[material as usize] += amount;
}
}
}

type Input = Vec<Blueprint>;


fn get_blueprint_score(blueprint: &Blueprint, time_remaining: u32) -> u32 {
let state = SearchState { time_remaining, robots: [1,0,0,0], materials: [0,0,0,0] };
let max_materials = get_max_materials(blueprint);
run_for_blueprint(&state, blueprint, &max_materials, None, 0)
}

fn run_for_blueprint(
state: &SearchState,
blueprint: &Blueprint,
max_materials: &[u32],
prev_skipped: Option<&Vec<usize>>,
best_so_far: u32
) -> u32 {
if state.time_remaining == 1 {
return state.materials[3] + state.robots[3];
}

if optimistic_best(state, Geode) < best_so_far {
return 0;
}

let min_obsidian = max_materials[2];
if optimistic_best(state, Obsidian) < min_obsidian {
return state.materials[3] + state.robots[3] * state.time_remaining;
}

let mut new_state = *state;
new_state.time_remaining -= 1;
(0..4).for_each(|i| new_state.materials[i] += new_state.robots[i]);

if state.can_build_robot(Geode as usize, blueprint, max_materials) {
new_state.build_robot(Geode as usize, blueprint);
return run_for_blueprint(&new_state, blueprint, max_materials, None, best_so_far);
}

let robots_available = (0..3).filter(|i| state.can_build_robot(*i, blueprint, max_materials)).collect_vec();
let mut best = best_so_far;

for &robot_type in &robots_available {
if prev_skipped.map(|ls| ls.contains(&robot_type)).unwrap_or(false) {
continue;
}

new_state.build_robot(robot_type, blueprint);
let score = run_for_blueprint(&new_state, blueprint, max_materials, None, best);
best = max(score, best);
new_state.un_build_robot(robot_type, blueprint);
}

let score = run_for_blueprint(&new_state, blueprint, max_materials, Some(&robots_available), best);
best = max(score, best);

best
}

fn optimistic_best(state: &SearchState, material: Material) -> u32 {
let mat = material as usize;
let i = state.time_remaining;

state.materials[mat] + state.robots[mat] * i + i * (i-1) / 2
}

fn get_max_materials(blueprint: &Blueprint) -> [u32; 4] {
let mut maxes = [0, 0, 0, u32::MAX];

for recipe in &blueprint.robot_recipes {
for &(amount, material) in recipe {
let i = material as usize;
maxes[i] = max(maxes[i], amount);
}
};
maxes
}


fn parse(input: &str) -> Input {
input.lines().filter_map(|line| {
// Blueprint 1: Each ore robot costs 4 ore. Each clay robot costs 4 ore. Each obsidian robot costs 4 ore and 5 clay. Each geode robot costs 3 ore and 7 obsidian.
let regex = Regex::new(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.").unwrap();
let c = regex.captures(line)?;
let mut captures = c.iter().skip(1);

let id = captures.next()??.as_str().parse().unwrap();

let ore_robot_ore_cost = captures.next()??.as_str().parse().unwrap();
let clay_robot_ore_cost = captures.next()??.as_str().parse().unwrap();
let obsidian_robot_ore_cost = captures.next()??.as_str().parse().unwrap();
let obsidian_robot_clay_cost = captures.next()??.as_str().parse().unwrap();
let geode_robot_ore_cost = captures.next()??.as_str().parse().unwrap();
let geode_robot_obsidian_cost = captures.next()??.as_str().parse().unwrap();

let ore_robot: Vec<(u32, Material)> = vec![(ore_robot_ore_cost, Ore)];
let clay_robot: Vec<(u32, Material)> = vec![(clay_robot_ore_cost, Ore)];
let obsidian_robot: Vec<(u32, Material)> = vec![(obsidian_robot_ore_cost, Ore), (obsidian_robot_clay_cost, Clay)];
let geode_robot: Vec<(u32, Material)> = vec![(geode_robot_ore_cost, Ore), (geode_robot_obsidian_cost, Obsidian)];

Some(Blueprint {
id,
robot_recipes: [ore_robot, clay_robot, obsidian_robot, geode_robot],
})
}).collect_vec()
}

pub fn part_one(input: Input) -> Option<u32> {
Some(
input.par_iter()
.map(|bp| (bp.id * get_blueprint_score(bp, 24)))
.sum::<u32>()
)
}

pub fn part_two(input: Input) -> Option<u32> {
Some(
input.par_iter()
.take(3)
.map(|bp| get_blueprint_score(bp, 32))
.product::<u32>()
)
}

解析器

每一行使用正则来获取数值,并且把这些数值变成 Blueprint 的结构体。

第一部分

就是一个简单的分支和切分支的算法。对于每个状态找出下一个可能的状态并对其做比较。其中的优化包括

  1. 如果分支的最大可能分数比现有最好的分支还低,那就不需要存在了。
  2. 当晶洞机器人能够被建造时候,尽快建造晶洞机器人,因为最终目标就是在最早的时间建造最多的晶洞机器人。
  3. 当一个分支没有足够时间产生足够的黑曜石来建造第一晶洞机器人时,提早结束这个分支
  4. 当一种机器人每回合生产的数量已经足够每回合都建造一个最贵的配方时,不再建造这种机器人,因为你一回合只能消耗那么多材料
  5. 当一个分支尝试建造一个在上一个状态能够建造一种机器人,但实际上并没有建造时,切断这个分支

这里使用了 rayon 的并行迭代器,来并行计算蓝图的质量,比起普通迭代器大概快了五六倍。

第二部分

和第一部分一样,只是减少蓝图的数量以及延长时间。

运行时间

1
2
3
4
5
Day 19
------
Parser: ✓ (17.9ms)
Part 1: 1981 (5.9ms)
Part 2: 10962 (15.5ms)