Advent Of Code 2022 第二十一天 - 猴子数学

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

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

问题

按照指示解码文件, 然后找到精灵们的位置。

第一部分

猴子回来了! 你担心它们会再次试图偷你的东西,但看起来它们只是坚守阵地,对你发出各种猴子的声音。

最后,其中一只大象意识到你不会说猴子话,于是过来解释。原来,它们无意中听到你说要找到小树林;如果你回答它们的 谜语 ,它们可以告诉你一条捷径。

每只猴子都有一个工作:要么喊出一个特定的数字,要么喊出一个数学运算的结果。所有喊数字的猴子从一开始就知道自己的数字;但是,数学运算的猴子需要等待另外两只猴子喊出一个数字,而这两只猴子可能也在等待其他猴子

你的工作是在猴子们自己算出来之前,算出名为root的猴子将喊出的数字

比如说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root: pppw + sjmn
dbpl: 5
cczh: sllz + lgvd
zczc: 2
ptdq: humn - dvpt
dvpt: 3
lfqf: 4
humn: 5
ljgn: 2
sjmn: drzm * dbpl
sllz: 4
pppw: cczh / lfqf
lgvd: ljgn * ptdq
drzm: hmdt - zczc
hmdt: 32

每一行都包含一只猴子的名字,一个冒号,然后是这只猴子的工作。

  • 一个单独的数字意味着这只猴子的工作就是喊出这个数字。
  • aaaa + bbbb这样的工作意味着猴子等待aaaabbbb分别喊出它们的数字;然后猴子喊出这两个数字的和。
  • aaaa - bbbb意味着猴子喊出aaaa的数字减去bbbb的数字。
  • 工作 aaaa * bbbb将喊出aaaa的数字乘以bbbb的数字。
  • 工作 aaaa / bbbb将喊出aaaa的数字除以bbbb的数字。

因此,在上面的例子中,猴子drzm必须等待猴子hmdtzczc喊出他们的号码。幸运的是,hmdtzczc的工作都是简单地喊出一个数字,所以他们马上就会这样做。322。猴子drzm可以通过找到32减去2来喊出它的数字: 30

然后,猴子sjmn有它的一个数字(30,来自猴子drzm),并且已经有它的另一个数字,5,来自dbpl。这使得它可以通过找到30乘以5来喊出自己的数字: 150

这个过程一直持续到root喊出一个数字: 152

然而,你的实际情况涉及相当多的猴子。名为root的猴子会喊出什么数字?

第二部分

由于某种猴子-大象-人类的错误翻译,你似乎误解了关于这个谜语的几个关键细节。

首先,你给名为 root 的猴子找错了工作;具体来说,你找错了数学运算。猴子 root 的正确操作应该是 = ,这意味着它仍然在监听两个数字(和以前一样来自两只猴子),但现在要检查这两个数字是否匹配

第二,你为以humn:开头的工作找错了猴子。这不是一只猴子–这是。事实上,你的工作也错了:你需要弄清楚你需要喊什么数字,以便root的检查通过。(在你的输入中出现在humn:后面的数字现在已经不重要了)。

在上面的例子中,你需要喊出的数字是 301 以通过root的检验。(这使得root从它的两只猴子那里得到相同的数字,150。)

你喊什么数字才能通过root的测试?

代码

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

21.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
use std::{collections::HashMap};

type MonkeyId = u32;

static ROOT_ID: MonkeyId = 0x746F6F72;
static HUMN_ID: MonkeyId = 0x6E6D7568;

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Op {
Add,
Sub,
Mul,
Div,
}

impl Op {
fn perform_op(&self, a: i64, b: i64) -> i64 {
match self {
Op::Add => a + b,
Op::Sub => a - b,
Op::Mul => a * b,
Op::Div => a / b,
}
}
}

#[derive(Debug, PartialEq, Clone)]
pub enum Monkey {
Yell(i64),
Parent(Box<(Monkey, Op, Monkey)>),
Human(i64),
}

impl Monkey {
fn from_str(id: MonkeyId, known_values: &HashMap<MonkeyId, String>) -> Self {
let s = known_values.get(&id).unwrap();
if s.chars().filter(|c| c.is_numeric()).count() > 0 {
let n = s.parse::<i64>().unwrap();
if id == HUMN_ID {
Monkey::Human(n)
} else {
Monkey::Yell(n)
}
} else {
let mut args = s.splitn(3, ' ');
let lhs = Monkey::from_str(str_id_to_monkey_id(args.next().unwrap()), known_values);
let op = match args.next().unwrap() {
"+" => Op::Add,
"-" => Op::Sub,
"*" => Op::Mul,
"/" => Op::Div,
_ => panic!("\"{}\" is not a valid math operator", args.next().unwrap()),
};
let rhs = Monkey::from_str(str_id_to_monkey_id(args.next().unwrap()), known_values);
Monkey::Parent(Box::new((lhs, op, rhs)))
}
}

fn into_children(self) -> (Monkey, Op, Monkey) {
if let Monkey::Parent(b) = self {
*b
} else {
panic!("attempted to get children of a non-parent")
}
}

fn value(&self) -> i64 {
match self {
Monkey::Yell(n) | Monkey::Human(n) => *n,
Monkey::Parent(b) => {
let (lhs, op, rhs) = b.as_ref();
op.perform_op(lhs.value(), rhs.value())
}
}
}

fn make_constant(&mut self) {
if let Monkey::Parent(b) = self {
let (lhs, op, rhs) = b.as_mut();
lhs.make_constant();
rhs.make_constant();
if let (Monkey::Yell(a), Monkey::Yell(b)) = (lhs, rhs) {
*self = Monkey::Yell(op.perform_op(*a, *b));
}
}
}

fn undo_op(self, h: &mut i64) -> Monkey {
let (lhs, op, rhs) = self.into_children();
match op {
Op::Add => {
let (cons, var) = if matches!(lhs, Monkey::Yell(..)) {
(lhs, rhs)
} else {
(rhs, lhs)
};
*h -= cons.value();
var
}
Op::Sub => {
if let Monkey::Yell(n) = lhs {
*h = n - *h;
rhs
} else if let Monkey::Yell(n) = rhs {
*h += n;
lhs
} else {
unreachable!()
}
}
Op::Mul => {
let (cons, var) = if matches!(lhs, Monkey::Yell(..)) {
(lhs, rhs)
} else {
(rhs, lhs)
};
*h /= cons.value();
var
}
Op::Div => {
if let Monkey::Yell(n) = lhs {
*h = n / *h;
rhs
} else if let Monkey::Yell(n) = rhs {
*h *= n;
lhs
} else {
unreachable!()
}
}
}
}

fn solve_for_humn(mut self) -> i64 {
self.make_constant();
let (lhs, _, rhs) = self.into_children();
let (constant_side, mut human_side) = if matches!(lhs, Monkey::Yell(..)) {
(lhs, rhs)
} else {
(rhs, lhs)
};
let mut h = constant_side.value();
while !matches!(human_side, Monkey::Human(..)) {
human_side = human_side.undo_op(&mut h);
}
h
}
}

type Input = Monkey;

fn parse(input: &str) -> Input {
let known_values = input.lines().filter_map(|line| {
let (id, yell_str) = line.split_once(": ")?;
Some((str_id_to_monkey_id(id), yell_str.to_string()))
}).collect();

Monkey::from_str(ROOT_ID, &known_values)
}

fn str_id_to_monkey_id(id: &str) -> MonkeyId {
let id = id.as_bytes();
u32::from_le_bytes([id[0], id[1], id[2], id[3]])
}

pub fn part_one(input: Input) -> Option<i64> {
Some(input.value())
}

pub fn part_two(input: Input) -> Option<i64> {
Some(input.solve_for_humn())
}

解析器

这里使用了一个 enum 来表示表达式中的每一个节点, 有三种类型:

  • Yell: 一个 i64 类型的值, 代表猴子喊出的数值
  • Human: 一个 i64 类型的值, 代表这一个 humn 的值
  • Parent: 一个包含两个 MonkeyBox, 分别是左子树, 运算符, 右子树

第一部分

在解析的时候, 我们使用了一个递归的方法, 从根节点开始, 逐步解析出整个表达式树。

第二部分

在第二部分中, 我们需要求出 humn 的值, 但是我们并不知道它的值, 所以我们需要先找到根节点的左子树还是右子树含有 humn

然后我们将两边都变成常量, 预先计算出所有的值, 然后反向求解 humn 的值。

运行时间

1
2
3
4
5
Day 21
------
Parser: ✓ (759.5µs)
Part 1: 353837700405464 (117.3µs)
Part 2: 3678125408017 (100.1µs)