Advent Of Code 2022 第十六天 - 普罗旺斯科火山岩

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

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

问题

优化结果问题。

第一部分

传感器将你引向了求救信号的源头:另一个手持设备,就像精灵们给你的那个。然而,你没有看到周围有任何精灵;相反,这个装置被大象包围了!他们一定是在这些隧道里迷路了。他们一定是在这些隧道里迷路了,而其中一头大象显然知道如何打开求救信号。

地面再次发出隆隆声,这次强烈得多。这到底是个什么样的山洞?你用你的手持设备扫描了这个洞穴;它报告说大部分是火成岩,一些灰烬,小块加压气体,岩浆……这不仅仅是一个洞穴,它是一座火山!你需要把大象弄出来。

你需要让大象离开这里,快点。你的设备估计,在火山爆发之前,你还有 30分钟 ,所以你没有时间从进来的路回去。

你扫视了一下山洞,寻找其他的选择,发现了一个管道和释放压力的 阀网络 。你不确定这样一个系统是如何进入火山的,但你没有时间抱怨;你的设备产生了一份报告(你的谜题输入),其中包括每个阀门如果它被打开的 流速 (以每分钟的压力计),以及你可以用来在阀门之间移动的隧道。

在你和大象目前所处的房间里,还有一个标有 AA 的阀门。你估计打开一个阀门需要1分钟,从一个阀门到另一个阀门的任何通道都需要1分钟。你能释放的最大压力是多少?

例如,假设你有以下的扫描输出:

1
2
3
4
5
6
7
8
9
10
Valve AA has flow rate=0; tunnels lead to valves DD, II, BB
Valve BB has flow rate=13; tunnels lead to valves CC, AA
Valve CC has flow rate=2; tunnels lead to valves DD, BB
Valve DD has flow rate=20; tunnels lead to valves CC, AA, EE
Valve EE has flow rate=3; tunnels lead to valves FF, DD
Valve FF has flow rate=0; tunnels lead to valves EE, GG
Valve GG has flow rate=0; tunnels lead to valves FF, HH
Valve HH has flow rate=22; tunnel leads to valve GG
Valve II has flow rate=0; tunnels lead to valves AA, JJ
Valve JJ has flow rate=21; tunnel leads to valve II

所有的阀门开始都是 关闭 的。你从AA阀开始,但它一定是损坏或卡住了或其他什么:它的流量是0,所以没有必要打开它。然而,你可以花一分钟时间移动到BB阀,再花一分钟时间打开它;这样做将在剩余的 28分钟 内释放压力,流量为13,最终的压力释放总量为 28*13=364 。然后,你可以用第三分钟移动到CC阀门,第四分钟打开它,以2的流量提供额外的 26分钟 的最终压力释放,CC阀门释放的总压力为 52

像这样穿过隧道,在30分钟过去时,你可能会打开许多或所有的阀门。然而,你需要尽可能多地释放压力,所以你需要有条不紊地进行。相反,可以考虑这种方法。

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
== Minute 1 ==
No valves are open.
You move to valve DD.

== Minute 2 ==
No valves are open.
You open valve DD.

== Minute 3 ==
Valve DD is open, releasing 20 pressure.
You move to valve CC.

== Minute 4 ==
Valve DD is open, releasing 20 pressure.
You move to valve BB.

== Minute 5 ==
Valve DD is open, releasing 20 pressure.
You open valve BB.

== Minute 6 ==
Valves BB and DD are open, releasing 33 pressure.
You move to valve AA.

== Minute 7 ==
Valves BB and DD are open, releasing 33 pressure.
You move to valve II.

== Minute 8 ==
Valves BB and DD are open, releasing 33 pressure.
You move to valve JJ.

== Minute 9 ==
Valves BB and DD are open, releasing 33 pressure.
You open valve JJ.

== Minute 10 ==
Valves BB, DD, and JJ are open, releasing 54 pressure.
You move to valve II.

== Minute 11 ==
Valves BB, DD, and JJ are open, releasing 54 pressure.
You move to valve AA.

== Minute 12 ==
Valves BB, DD, and JJ are open, releasing 54 pressure.
You move to valve DD.

== Minute 13 ==
Valves BB, DD, and JJ are open, releasing 54 pressure.
You move to valve EE.

== Minute 14 ==
Valves BB, DD, and JJ are open, releasing 54 pressure.
You move to valve FF.

== Minute 15 ==
Valves BB, DD, and JJ are open, releasing 54 pressure.
You move to valve GG.

== Minute 16 ==
Valves BB, DD, and JJ are open, releasing 54 pressure.
You move to valve HH.

== Minute 17 ==
Valves BB, DD, and JJ are open, releasing 54 pressure.
You open valve HH.

== Minute 18 ==
Valves BB, DD, HH, and JJ are open, releasing 76 pressure.
You move to valve GG.

== Minute 19 ==
Valves BB, DD, HH, and JJ are open, releasing 76 pressure.
You move to valve FF.

== Minute 20 ==
Valves BB, DD, HH, and JJ are open, releasing 76 pressure.
You move to valve EE.

== Minute 21 ==
Valves BB, DD, HH, and JJ are open, releasing 76 pressure.
You open valve EE.

== Minute 22 ==
Valves BB, DD, EE, HH, and JJ are open, releasing 79 pressure.
You move to valve DD.

== Minute 23 ==
Valves BB, DD, EE, HH, and JJ are open, releasing 79 pressure.
You move to valve CC.

== Minute 24 ==
Valves BB, DD, EE, HH, and JJ are open, releasing 79 pressure.
You open valve CC.

== Minute 25 ==
Valves BB, CC, DD, EE, HH, and JJ are open, releasing 81 pressure.

== Minute 26 ==
Valves BB, CC, DD, EE, HH, and JJ are open, releasing 81 pressure.

== Minute 27 ==
Valves BB, CC, DD, EE, HH, and JJ are open, releasing 81 pressure.

== Minute 28 ==
Valves BB, CC, DD, EE, HH, and JJ are open, releasing 81 pressure.

== Minute 29 ==
Valves BB, CC, DD, EE, HH, and JJ are open, releasing 81 pressure.

== Minute 30 ==
Valves BB, CC, DD, EE, HH, and JJ are open, releasing 81 pressure.

这种方法可以让你在30分钟内用这种阀门布局释放最大的压力, 1651

计算出在30分钟内释放最大压力的步骤。 你能释放的最大压力是多少?

第二部分

你担心即使采用最佳方法,释放的压力也是不够的。如果你让其中一头大象来帮助你呢?

你需要花4分钟来教一头大象如何按照正确的顺序打开正确的阀门,而你只有 26分钟 来真正执行你的计划。有两个人一起工作会不会更好,即使这意味着有更少的时间?(假设你先教大象,然后再自己打开任何阀门,那么你们两个人同样有整整26分钟的时间)。

在上面的例子中,你可以教大象来帮助你,如下所示。

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
== Minute 1 ==
No valves are open.
You move to valve II.
The elephant moves to valve DD.

== Minute 2 ==
No valves are open.
You move to valve JJ.
The elephant opens valve DD.

== Minute 3 ==
Valve DD is open, releasing 20 pressure.
You open valve JJ.
The elephant moves to valve EE.

== Minute 4 ==
Valves DD and JJ are open, releasing 41 pressure.
You move to valve II.
The elephant moves to valve FF.

== Minute 5 ==
Valves DD and JJ are open, releasing 41 pressure.
You move to valve AA.
The elephant moves to valve GG.

== Minute 6 ==
Valves DD and JJ are open, releasing 41 pressure.
You move to valve BB.
The elephant moves to valve HH.

== Minute 7 ==
Valves DD and JJ are open, releasing 41 pressure.
You open valve BB.
The elephant opens valve HH.

== Minute 8 ==
Valves BB, DD, HH, and JJ are open, releasing 76 pressure.
You move to valve CC.
The elephant moves to valve GG.

== Minute 9 ==
Valves BB, DD, HH, and JJ are open, releasing 76 pressure.
You open valve CC.
The elephant moves to valve FF.

== Minute 10 ==
Valves BB, CC, DD, HH, and JJ are open, releasing 78 pressure.
The elephant moves to valve EE.

== Minute 11 ==
Valves BB, CC, DD, HH, and JJ are open, releasing 78 pressure.
The elephant opens valve EE.

(At this point, all valves are open.)

== Minute 12 ==
Valves BB, CC, DD, EE, HH, and JJ are open, releasing 81 pressure.

...

== Minute 20 ==
Valves BB, CC, DD, EE, HH, and JJ are open, releasing 81 pressure.

...

== Minute 26 ==
Valves BB, CC, DD, EE, HH, and JJ are open, releasing 81 pressure.

在大象的帮助下,26分钟后,你能做的最好的事就是释放出总共 1707 的压力。

你和大象一起工作了26分钟,你能释放的最大压力是多少?

代码

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

16.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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
use itertools::Itertools;
use std::cmp::Reverse;
use std::collections::HashMap;

type FlowRates = Vec<u8>;
type FlowRateIndices = Vec<usize>;
type ShortestPathLengths = Vec<Vec<u8>>;

type Input = (FlowRates, ShortestPathLengths, FlowRateIndices, usize);

fn branch_and_bound(
flow_rates: &FlowRates,
sorted_flow_rate_indices: &[usize],
shortest_path_lengths: &ShortestPathLengths,
state: State,
best_for_visited: &mut [u16],
best: &mut u16,
filter_bound: impl Fn(u16, u16) -> bool + Copy,
) {
if let Some(cur_best) = best_for_visited.get_mut(state.visited as usize) {
*cur_best = state.pressure_released.max(*cur_best);
}
*best = state.pressure_released.max(*best);

state
.branch(flow_rates, shortest_path_lengths)
.into_iter()
.map(|state| (state.bound(flow_rates, sorted_flow_rate_indices), state))
.filter(|&(bound, _)| filter_bound(bound, *best))
.sorted_unstable_by_key(|(bound, _)| Reverse(*bound))
.for_each(|(_, branch)| {
branch_and_bound(
flow_rates,
sorted_flow_rate_indices,
shortest_path_lengths,
branch,
best_for_visited,
best,
filter_bound,
)
});
}

#[derive(Default, Debug, Clone, Copy)]
struct State {
visited: u16,
avoid: u16,
pressure_released: u16,
minutes_remaining: u8,
position: u8,
}

impl State {
fn new(position: u8, minutes_remaining: u8) -> Self {
Self {
visited: 0,
avoid: 1 << position,
pressure_released: 0,
minutes_remaining,
position,
}
}

fn can_visit(self, i: usize) -> bool {
(self.visited | self.avoid) & (1 << i) == 0
}

fn bound(self, flow_rates: &FlowRates, sorted_flow_rate_indices: &[usize]) -> u16 {
self.pressure_released
+ (0..=self.minutes_remaining)
.rev()
.step_by(2)
.skip(1)
.zip(
sorted_flow_rate_indices
.iter()
.filter(|&&i| self.can_visit(i))
.map(|&i| flow_rates[i]),
)
.map(|(minutes, flow)| minutes as u16 * flow as u16)
.sum::<u16>()
}

fn branch<'a>(
self,
flow_rates: &'a FlowRates,
shortest_path_lengths: &'a ShortestPathLengths,
) -> impl IntoIterator<Item = Self> + 'a {
shortest_path_lengths[self.position as usize]
.iter()
.enumerate()
.filter(move |&(destination, _distance)| self.can_visit(destination))
.filter_map(move |(destination, distance)| {
let minutes_remaining = self.minutes_remaining.checked_sub(*distance + 1)?;
Some(State {
visited: self.visited | (1 << destination),
avoid: self.avoid,
pressure_released: self.pressure_released
+ minutes_remaining as u16 * flow_rates[destination] as u16,
minutes_remaining,
position: destination as u8,
})
})
}
}

fn floyd_warshall(rows: &[(&str, u8, Vec<&str>)]) -> Vec<Vec<u8>> {
let valve_name_to_idx: HashMap<&str, _> = rows
.iter()
.enumerate()
.map(|(i, &(name, _, _))| (name, i))
.collect();

let mut dist = vec![vec![u8::MAX; rows.len()]; rows.len()];
for (i, (_, _, tunnels)) in rows.iter().enumerate() {
for tunnel in tunnels {
let j = valve_name_to_idx[tunnel];
dist[i][j] = 1;
}
}
(0..dist.len()).for_each(|i| {
dist[i][i] = 0;
});
for k in 0..dist.len() {
for i in 0..dist.len() {
for j in 0..dist.len() {
let (result, overflow) = dist[i][k].overflowing_add(dist[k][j]);
if !overflow && dist[i][j] > result {
dist[i][j] = result;
}
}
}
}
dist
}

fn parse_row(row: &str) -> (&str, u8, Vec<&str>) {
//Valve VR has flow rate=11; tunnels lead to valves LH, KV, BP
let (a, b) = row.split_once(" has flow rate=").unwrap();
let (b, c) = b.split_once(" to ").expect(b);
let b = match b.strip_suffix("; tunnels lead") {
Some(b) => b,
None => b.strip_suffix("; tunnel leads").expect(b),
};
let c = match c.strip_prefix("valves ") {
Some(c) => c,
None => c.strip_prefix("valve ").unwrap(),
};
(
a.strip_prefix("Valve ").unwrap(),
b.parse().unwrap(),
c.split(", ").collect(),
)
}

fn parse(input: &str) -> Input {
let rows = input.lines().map(parse_row).collect_vec();
let shortest_path_lengths_uncompressed = floyd_warshall(&rows);

let interesting_valve_indices = rows
.iter()
.enumerate()
.filter(|&(_, &(name, flow, _))| name == "AA" || flow > 0)
.map(|(i, _)| i)
.collect_vec();

let flow_rates = interesting_valve_indices
.iter()
.map(|&i| rows[i].1)
.collect_vec();

let shortest_path_lengths = interesting_valve_indices
.iter()
.map(|&i| {
interesting_valve_indices
.iter()
.map(|&j| shortest_path_lengths_uncompressed[i][j])
.collect()
})
.collect();

let starting_node = interesting_valve_indices
.iter()
.position(|&i| rows[i].0 == "AA")
.expect("a valve called AA");

let sorted_flow_rate_indices = flow_rates
.iter()
.enumerate()
.sorted_unstable_by_key(|&(_, &flow)| Reverse(flow))
.map(|(i, _)| i)
.collect_vec();

(
flow_rates,
shortest_path_lengths,
sorted_flow_rate_indices,
starting_node,
)
}

pub fn part_one(
(flow_rates, shortest_paths, sorted_flow_rate_indices, starting_idx): Input,
) -> Option<u16> {
let mut best = 0;
branch_and_bound(
&flow_rates,
&sorted_flow_rate_indices,
&shortest_paths,
State::new(starting_idx as u8, 30),
&mut [],
&mut best,
|bound, best| bound > best,
);
Some(best)
}

pub fn part_two(
(flow_rates, shortest_paths, sorted_flow_rate_indices, starting_idx): Input,
) -> Option<u16> {
let mut best_per_visited = vec![0; u16::MAX as usize];
branch_and_bound(
&flow_rates,
&sorted_flow_rate_indices,
&shortest_paths,
State::new(starting_idx as u8, 26),
&mut best_per_visited,
&mut 0,
|bound, best| bound > best,
);
let mut best = 0;
let best_per_visited_filtered_sorted = best_per_visited
.into_iter()
.enumerate()
.filter(|&(_, best)| best > 0)
.map(|(i, best)| (i as u16, best))
.sorted_unstable_by_key(|&(_, best)| Reverse(best))
.collect_vec();

for (i, &(my_visited, my_best)) in best_per_visited_filtered_sorted.iter().enumerate() {
for &(elephant_visited, elephant_best) in &best_per_visited_filtered_sorted[i + 1..] {
let score = my_best + elephant_best;
if score <= best {
break;
}
if my_visited & elephant_visited == 0 {
best = score;
break;
}
}
}
Some(best)
}

解析器

提取流速,过滤出流速为 0 的阀门,并通过 Floyd Warshall 算法预计算最短距离给之后的 bound 做计算。

第一部分

使用了 Branch and Bound 算法来解决问题,简单来说就是靠边界来大幅缩小查找空间,从而减少搜索时间。为了解决这个问题,我不得不在维基百科上阅读旅行推销员问题,并复制其中一种推荐的算法。我学到了很多!

这里的位操作是为了存储状态,使用了一个 16 位的 bitmap。

如果你不做优化直接 DP ,状态非常的多,几分钟都出不来答案。

第二部分

同样的,你理论上可以通过 DP 找到最佳状态,但是这里就算用第一部分的方法,也需要很长时间。

但是后来发现,计算每组访问过的阀门在26分钟内的最佳压力一次就足够了,然后可以将访问过的一组不相交的阀门的两个解决方案结合起来,得到最终结果。

我不知道为什么第二部分的时间比第一部分还低,大概是编译器的优化缓存了结果吧。

运行时间

1
2
3
4
5
Day 16
------
Parser: ✓ (532.3µs)
Part 1: 1880 (694.0µs)
Part 2: 2520 (320.5µs)