Advent Of Code 2022 第五天 - 供应堆栈

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

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

问题

移动箱子部分不是很复杂,只要按照指示操作就好,这题比较有趣的地方在于写出解析初始状态的解析器。

第一部分

一旦最后的物资从船上卸下,探险队就可以出发了。补给品被存放在一堆有标记的 中,但由于需要的补给品被埋在许多其他箱子下面,所以箱子需要重新排列。

船上有一个 巨大的货物起重机 ,能够在堆栈之间移动板条箱。为了确保没有一个板条箱被压碎或倒下,起重机操作员将通过一系列精心策划的步骤重新安排它们。在板条箱被重新排列后,所需的板条箱将在每个堆栈的顶部。

精灵们不想在这个微妙的过程中打断起重机操作员,但他们忘了问她 哪个 板条箱最后会在哪里,而且他们想尽快准备好卸货,以便他们能够登船。

然而,他们确实有一张开始堆放的板条箱的图纸 重新排列的程序(你的谜题输入)。比如说:

1
2
3
4
5
6
7
8
9
    [D]    
[N] [C]
[Z] [M] [P]
1 2 3

move 1 from 2 to 1
move 3 from 1 to 3
move 2 from 2 to 1
move 1 from 1 to 2

在这个例子中,有三个堆栈的板条箱。堆栈1包含两个箱子:箱子Z在底部,箱子N
在顶部。堆栈2包含三个箱子;从下到上,它们是箱子MC,和D。最后,堆栈3包含一个板条箱,P

然后,给出了重新排列的程序。在程序的每一步中,一定数量的板条箱被从一个堆栈移到另一个堆栈。在上述重排程序的第一步中,一个板条箱从堆栈2移到堆栈1,形成了这种配置。

1
2
3
4
[D]        
[N] [C]
[Z] [M] [P]
1 2 3

在第二步中,三个板条箱被从堆栈1移到堆栈3。板条箱是 一个个移动 的,所以第一个被移动的板条箱(D)最后会在第二和第三个板条箱下面。

1
2
3
4
5
       [Z]
[N]
[C] [D]
[M] [P]
1 2 3

然后,两个板条箱都从堆栈2移到堆栈1。同样,因为箱子是 一个个移动 的,所以箱子C最后会在箱子M下面。

1
2
3
4
5
        [Z]
[N]
[M] [D]
[C] [P]
1 2 3

最后,一个板条箱从堆栈1移到堆栈2。

1
2
3
4
5
        [Z]
[N]
[D]
[C] [M] [P]
1 2 3

精灵们只需要知道 哪个板条箱最终会在每个堆栈的顶部 ;在这个例子中,顶部的板条箱是堆栈1中的C,堆栈2中的M
,以及堆栈3中的Z,所以你应该把这些结合起来,给精灵们一个信息 CMZ

在重新排列程序完成后,每个堆栈顶部的板条箱最终是什么?

第二部分

当你看着起重机操作员熟练地重新排列板条箱时,你注意到这个过程并没有按照你的预测进行。

一些泥巴覆盖在起重机侧面的字迹上,你赶紧把它擦掉。这台起重机不是 箱子移动机9000 - 它是 箱子移动机9001

箱子移动机9001值得注意的是许多新的和令人兴奋的功能:空调、皮革座椅、一个额外的杯架,以及 能够同时拿起和移动多个箱子

再次考虑上面的例子,板条箱开始时的配置是一样的。

1
2
3
4
    [D]    
[N] [C]
[Z] [M] [P]
1 2 3

将单个板条箱从堆栈2移到堆栈1的行为与以前一样。

1
2
3
4
[D]        
[N] [C]
[Z] [M] [P]
1 2 3

然而,将三个板条箱从堆栈1移到堆栈3的动作意味着这三个移动的板条箱*保持相同的顺序,结果是这样的新配置。

1
2
3
4
5
       [D]
[N]
[C] [Z]
[M] [P]
1 2 3

接下来,当两个箱子从堆栈2移到堆栈1时,它们也 保持它们的顺序

1
2
3
4
5
        [D]
[N]
[C] [Z]
[M] [P]
1 2 3

最后,一个板条箱仍然从堆栈1移到堆栈2,但现在是板条箱C被移动。

1
2
3
4
5
        [D]
[N]
[Z]
[M] [C] [P]
1 2 3

在这个例子中,箱子移动机9001把板条箱放在一个完全不同的顺序。 MCD

在重排过程结束前,更新你的模拟,让精灵们知道他们应该站在哪里,以准备卸下最后的物资。

在重新安排程序完成后,什么板条箱最终会在每个堆栈的顶部?

代码

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

05.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
use itertools::Itertools;

type Input = (Stack, Vec<Instruction>);

type Stack = Vec<Vec<char>>;
type Instruction = (usize, usize, usize);

fn move_stacks(stack: &mut Stack, instruction: Vec<Instruction>, grouped: bool) {
instruction.iter().for_each(|&(count, from, to)| {
let pillar = &mut stack[from - 1];
let boxes = pillar.split_off(pillar.len() - count);
if grouped {
stack[to - 1].extend(boxes.iter())
} else {
stack[to - 1].extend(boxes.iter().rev())
}
})
}

fn top_row_string(stack: Stack) -> String {
stack.iter().filter_map(|pillar| pillar.last()).join("")
}

fn parse(input: &str) -> Input {
let (stack_input, instruction_input) = input.split_once("\n\n").unwrap();
let mut stack_iter = stack_input.lines().rev();
let mut stack = vec![vec![]; stack_iter.next().unwrap().len() / 4 + 1];

stack_iter.for_each(|line| {
line.chars()
.skip(1)
.step_by(4)
.enumerate()
.for_each(|(i, x)| if x != ' ' { stack[i].push(x) })
});

let instructions = instruction_input
.lines()
.map(|x| {
let k = x.split_ascii_whitespace().collect::<Vec<&str>>();
(k[1].parse().unwrap(), k[3].parse().unwrap(), k[5].parse().unwrap())
}).collect();
(stack, instructions)
}

pub fn part_one(input: Input) -> Option<String> {
let (mut stack, instruction) = input;
move_stacks(&mut stack, instruction, false);
Some(
top_row_string(stack)
)
}

pub fn part_two(input: Input) -> Option<String> {
let (mut stack, instruction) = input;
move_stacks(&mut stack, instruction, true);
Some(
top_row_string(stack)
)
}

解析器

这个是这天的重点,很多人都没写解析器就靠手输入,毕竟这初始状态一看就不好做。 我这解析器是把输入分为两部分解决。

初始状态先获取有多少个柱子,然后从下往上每一行从第二个字符开始跳过四个,正好就是每一个柱子的输入,如果是空格就无视。

指示的部分就很简单了,只需要用空格分割然后找出对的地方就可以了。

第一部分

一个个拿起来如果你想象一下结果,就跟拿起来了反转一下一样。

第二部分

整块拿起来连反转都不需要。

运行时间

所有时间由我这垃圾笔记本电脑进行(不科学的)测试,均以 --release 启动。

1
2
3
4
5
Day 5
------
Parser: ✓ (211.9µs)
Part 1: TDCHVHJTG (57.6µs)
Part 2: NGCMPJLHV (51.8µs)