开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情
- Advent of Code 2020 Day6 译文(用 Rust 实现 Advent of Code 2020 第6天)
- 原文链接:fasterthanli.me/series/adve…
- 原文作者:Amos
- 译文来自:github.com/suhanyujie/…
- 译者:suhanyujie
- ps:水平有限,如有不当之处,欢迎指正
- 标签:Rust,advent of code, algo
离 Advent of Code 2020 结束,我们还有很长一段路要走。第六天的时间到了。
这里的问题描述有点做“作”,就像之前那些题目一样,但这并不能阻止我们。
题目的输入如下:
abc
a
b
c
ab
ac
a
a
a
a
b
每一行代表一个人,“人群”用空白行分隔。
有 26 个可能的字母,即 a 到 z。
酷熊:我们要声明一个有 26 种变体的枚举吗?
Amos:emmm,用一个字节就够了
因此,第一部分是收集组(group)中的每个人填写的答案 —— 我们甚至不需要解析器!(译注:如果不明白题意,可以看下原题的详细介绍。)
use std::collections::HashSet;
fn main() {
let answers: Vec<_> = include_str!("input.txt")
.split("\n\n")
.map(|group| {
group
.lines()
.map(|line| {
let mut set = HashSet::new();
for &b in line.as_bytes() {
set.insert(b);
}
set
})
.collect::<Vec<_>>()
})
.collect();
dbg!(&answers[0..3]);
}
$ cargo run --quiet
[src/main.rs:20] &answers[0..2] = [
[
{
98,
},
{
98,
},
{
98,
},
{
98,
},
],
[
{
120,
},
{
120,
102,
106,
107,
},
{
120,
98,
},
],
]
酷熊:输出感觉不太可读 - 难道我们不能使用一个新类型,这样我们就可以输出自己的调试信息?
use std::{collections::HashSet, fmt};
pub struct Answers(HashSet<u8>);
impl fmt::Debug for Answers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for &answer in &self.0 {
write!(f, "{}", answer as char)?;
}
Ok(())
}
}
fn main() {
let answers: Vec<_> = include_str!("input.txt")
.split("\n\n")
.map(|group| {
group
.lines()
.map(|line| {
let mut set = HashSet::new();
for &b in line.as_bytes() {
set.insert(b);
}
Answers(set)
})
.collect::<Vec<_>>()
})
.collect();
dbg!(&answers[0..2]);
}
$ cargo run --quiet
[src/main.rs:31] &answers[0..2] = [
[
b,
b,
b,
b,
],
[
x,
xkfj,
bx,
],
]
酷熊:好! 我们可以比较题目输入来确定吗?
b
b
b
b
x
xfkj
xb
酷熊:太棒了!
现在我们要做的就是,为每组计算答案的集合的联合体(译注:多个集合的合并后的集合)。
在 std::collection::HashSet 上有一个 union 方法:
pub fn union<'a>(&'a self, other: &'a HashSet<T, S>) -> Union<'a, T, S>
但不幸的是,它计算的是两个 HashSet 的联合体,并返回一个迭代器。
酷熊:说到迭代器... 我们为什么要这样构造 HashSet?
let mut set = HashSet::new();
for &b in line.as_bytes() {
set.insert(b);
}
Answers(set)
酷熊:我们也许可以将其收集到一个 Vec 中... 而非收集到一个 HashSet?
Amos:当然可以!
let answers: Vec<_> = include_str!("input.txt")
.split("\n\n")
.map(|group| {
group
.lines()
.map(|line| Answers(line.as_bytes().iter().copied().collect()))
.collect::<Vec<_>>()
})
.collect();
酷熊:很好!对了,你刚说什么来着?
标准库提供了计算两个 HashSet 的并集并返回一个迭代器的方法。说实话,这不是什么大问题 —— 我们可以将所有迭代器链接起来,然后将得到的迭代器再合并到一个单独的 HashSet 中。
不过我们换个方式,试试 im crate。
它提供了一组不可变的数据结构,包括 HashSet!
$ cargo add im
Adding im v15.0.0 to dependencies
现在我们只要把“引入”改成如下形式:
use im::HashSet;
use std::fmt;
你看,一样可以执行:
$ cargo run --quiet
[src/main.rs:26] &answers[0..2] = [
[
b,
b,
b,
b,
],
[
x,
kxjf,
bx,
],
]
酷熊:呃,这里的不可变是指什么?
Amos:啥?
酷熊:好了好了,我们还是基于 HashSet 来实现吧:
.map(|line| Answers(line.as_bytes().iter().copied().collect()))
酷熊:这意味着它将创建了一个空的 im::HashSet,然后将元素逐个添加到其中,对吗?
Amos:是的。
酷熊:那不是可变吗?
Amos:我们看下 im 的文档。
所有这些数据结构都支持写时复制的可变,这意味着当一个数据结构只有一个地方在使用,你可以就地更新它,而无需在修改数据结构之前复制一个数据结构(这比不可变操作快了一个数量级,几乎和
std::Collection的可变数据结构一样快)。
酷熊:聪明!
Amos:是的,很聪明! 这个库的作者和我们最近研究的 smartstring 是同一个作者。
酷熊:但是,如果一个对象不是 HashSet 的唯一持有者,会发生什么情况呢?比如,把一个对象 clone 一下,给其他地方使用。
Amos:那这样就会发生写时复制(Cow)了!
由于
Rc的引用计数,我们能够确定一个数据结构中的一个节点是否与其他数据结构共享,或者在适当的位置对其进行安全地修改。当它被共享时,我们将在修改它之前自动创建一个副本。
这样做的结果是,把拷贝一个数据结构变成了一个延迟操作: 首次拷贝是即时的,当你修改克隆的数据结构时,它只会在你修改它们的地方发生拷贝,所以如果你修改了整个结构,你最终会完成一个完整的拷贝。
- 酷熊热辣小贴士
- 注意,
imcrate 中的所有数据结构都使用Arc,这意味着它们是线程安全的,但是,这样大约有“20-25%”的性能损失。- 有一个使用
Rc的im-rc变体,它具有更好的性能,但是数据结构不是线程安全的(它们既不符合Send也不符合Sync约束)。
无论如何, im 有一个我喜欢的 union 方法:
pub fn union(self, other: Self) -> Self
等等,不,我喜欢 unions 方法:
pub fn unions<I>(i: I) -> Self
where
I: IntoIterator<Item = Self>,
S: Default,
酷熊:所以我们只是... 我们只是使用 im::HashSet::unions 来 map 我们的 groups?
Amos:是的!
main 的内容如下:
fn main() {
let answers: Vec<_> = include_str!("input.txt")
.split("\n\n")
.map(|group| {
group
.lines()
.map(|line| Answers(line.as_bytes().iter().copied().collect()))
.collect::<Vec<_>>()
})
.collect();
let group_answers: Vec<_> = answers
.into_iter()
.map(|group| Answers(HashSet::unions(group.into_iter().map(|x| x.0))))
.collect();
dbg!(&group_answers[0..5]);
}
或者,如果我们跳过中间步骤:
fn main() {
let group_answers: Vec<_> = include_str!("input.txt")
.split("\n\n")
.map(|group| {
Answers(HashSet::unions(
group
.lines()
.map(|line| line.as_bytes().iter().copied().collect()),
))
})
.collect();
dbg!(&group_answers[0..5]);
}
酷熊:太棒了! 还有什么问题来着?
每个组(group)都有一定数量的“yes”答案(“yes”在集合中表示为一个字母)。
题目要求计算所有小组回答问题的总和。
酷熊:好! 我们可以使用 .sum()!我们先引入 itertools 和..
Amos:实际上,正如很多读者指出的那样,标准库中的 .sum() 也是可用的。
酷熊:那更好了!
我们可以跳过一些中间步骤:
fn main() {
let answer: usize = include_str!("input.txt")
.split("\n\n")
.map(|group| {
HashSet::<u8>::unions(
group
.lines()
.map(|line| line.as_bytes().iter().copied().collect()),
)
.len()
})
.sum();
dbg!(answer);
}
然后得到第一部分问题的答案:
$ cargo run --quiet
[src/main.rs:28] answer = 6534
第二部分
在接下来的部分中,the Advent of Code 将我们带入了旧的转换。
我们不需要计算小组中每个成员的答案的 unions,而是取交集 —— 也就是说,只保留小组中每个人都同意的答案。
酷熊:im 中是否有 intersections 相关方法?
Amos:很遗憾,没有... 但我们可以尝试 reduce!
酷熊:你是说 fold 吗?
Amos:没错,就是它!
fn main() {
let answer: usize = include_str!("input.txt")
.split("\n\n")
.map(|group| {
group
.lines()
.map(|line| line.as_bytes().iter().copied().collect())
.fold(HashSet::<u8>::new(), |acc, x| acc.intersection(x))
.len()
})
.sum();
dbg!(answer);
}
$ cargo run --quiet
[src/main.rs:27] answer = 0
酷熊:有点不对劲。
Amos:我不太确定有什么问题
酷熊:哦! HashSet::new() 返回一个空集!
Amos:啊,空集与其他集合的交集就是。。
酷熊:空集合!
Amos:好的,我们可以通过分离第一个元素来修复这个问题,并使用它作为 fold() 调用的初始值。
酷熊:或者只是让初始化时的 HashSet 包含所有可能的答案?
Amos:这主意不错。
fn main() {
let init: HashSet<u8> = (b'a'..=b'z').collect();
let answer: usize = include_str!("input.txt")
.split("\n\n")
.map(|group| {
group
.lines()
.map(|line| line.as_bytes().iter().copied().collect())
.fold(init.clone(), |acc, x| acc.intersection(x))
.len()
})
.sum();
dbg!(answer);
}
Amos:你知道吗? 这样做效果更好,因为 init.clone() 基本上是免费的!
酷熊:的确如此,因为它是不可变的?
Amos:没错!
酷熊:所以使用 im 是有特殊原因的?
Amos:都有一点,问题最后都会解决的。
酷熊:难道没有一种 fold 的变体,只是给第一个元素设置 init 吗?
Amos:事实证明,有! 只是不在标准库中。
$ cargo add itertools
Adding itertools v0.9.0 to dependencies
我们的解决方案是:
use im::HashSet;
use itertools::Itertools;
fn main() {
let answer: usize = include_str!("input.txt")
.split("\n\n")
.map(|group| {
group
.lines()
.map(|line| line.as_bytes().iter().copied().collect())
.fold1(|acc: HashSet<u8>, x| acc.intersection(x))
.unwrap_or_default()
.len()
})
.sum();
dbg!(answer);
}
酷熊:等等,为什么用 unwrap_or_default?
Amos:嗯。。如果我们 fold1 一个 0 项的集合会怎么样呢? 那么开始就没有 init 值了!
酷熊:啊,对!
$ cargo run --quiet
[src/main.rs:29] answer = 3402
酷熊:又对了,太有趣了!
Amos:下次见,熊!
酷熊:再见!