开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
- Advent of Code 2020 Day1 译文(用 Rust 实现 Advent of Code 2020 第一天)
- 原文链接:fasterthanli.me/series/adve…
- 原文作者:Amos
- 译文来自:github.com/suhanyujie/…
- 译者:suhanyujie
- ps:水平有限,如有不当之处,欢迎指正
- 标签:Rust,advent of code, algo
今年 12 月我还没有具体的计划,但是我周围的很多人(在 Twitter 上,在工作中)都选择了这个 Advent of Code 来学习 Rust,而且我有很大的 FOMO 情绪,所以,看看最后我能走多远。
我将在 Linux 上完成这一系列作品,因此可能会涉及到一些命令行工具,但不要担心 —— 代码理论上能在所有平台上运行。
如果你想继续,你需要安装带有 Rust Analyzer 扩展的 VS Code,并且你还需要安装 Rust。
在 Windows 上,你可能必须安装 VS2019 构建工具,这可能有点烦人 - 抱歉!
第一部分
问题的输入类似下方
1721
979
366
299
675
1456
我们的任务是找到两数相加的和为 2020 的项,然后把它们相乘,让我们开始吧!
$ cargo new day1
Created binary (application) `day1` package
首先,让我将题目输入粘贴到 day-1/src/input.txt 文件中:
1470
1577
1054
(cut)
1911
1282
1306
但你的文件名可能跟我不一样
然后,我们要读取这个文件,这里有几个选项,我们可以在运行时处理:
$ cargo add anyhow
Updating 'https://github.com/rust-lang/crates.io-index' index
Adding anyhow v1.0.35 to dependencies
小贴士:
cargo add由 cargo-edit 提供,我们将在整个系列中使用这个插件。 如果你按照以下步骤,可以简单地安装它并编辑依赖文件cargo install cargo-edit
// in `day1/src/main.rs`
fn main() -> anyhow::Result<()> {
let s = std::fs::read_to_string("./src/input.txt")?;
dbg!(&s[..40]);
Ok(())
}
这将读取文件,并打印前 40 个字节内容:
$ cargo run --quiet
[src/main.rs:3] &s[..40] = "1470\n1577\n1054\n1962\n1107\n1123\n1683\n1680\n"
... 但是在运行时打开文件可能会失败,我们需要确保 input.txt 文件总是在可执行文件的旁边。
因此,我们只需要在编译时将它包含进去:
fn main() -> anyhow::Result<()> {
let s = include_str!("input.txt");
dbg!(&s[..40]);
Ok(())
}
$ cargo run --quiet
[src/main.rs:3] &s[..40] = "1470\n1577\n1054\n1962\n1107\n1123\n1683\n1680\n"
现在字符串内容是可执行文件的一部分:
$ xxd target/debug/day1 | grep "1470.15" -A 5
0003b000: 4572 726f 723a 200a 3134 3730 0a31 3537 Error: .1470.157
0003b010: 370a 3130 3534 0a31 3936 320a 3131 3037 7.1054.1962.1107
0003b020: 0a31 3132 330a 3136 3833 0a31 3638 300a .1123.1683.1680.
0003b030: 3131 3736 0a31 3931 370a 3137 3836 0a31 1176.1917.1786.1
0003b040: 3536 350a 3134 3634 0a31 3039 370a 3133 565.1464.1097.13
0003b050: 3633 0a31 3039 310a 3130 3732 0a31 3832 63.1091.1072.182
小贴士: xxd 是大多数 Linux 发行版附带的一个非常基本(且古老)的 hexdump 工具。
到目前为止,我们有一个很大的 String。我们希望分别处理每一行,所以,让我们用换行符对其分割:
fn main() -> anyhow::Result<()> {
let s = include_str!("input.txt").split("\n");
Ok(())
}
小贴士: 你可能想用
.lines()代替.split("\n")进行拆分,以便这与 CRLF 行结束兼容(Windows)。本文的其余部分是按照split的思路编写的,但是它们都提供了迭代器,所以你可以随便选。
它给我们的不是一个字符串数组,而是一个实现了 Iterator<Item = &str> 的具体类型(Split<&str>)。
酷熊:等等,&str?我们不是有 String 吗?
Amos: 的确如此!我们的迭代器现在返回的元素是从原来的 String 中借来的。它们是来自原始 String 的片段,因此不涉及复制。
酷熊:等等,不!当我们使用 std::fs::read_to_string 时,我们确实有 String:
但是等等... ... 当我们使用 include_str! 时,我们得到的是 &str:
Amos: 哦,对了!当我们“将字符串打包进在可执行文件中”时,我们得到的也是借用的字符串,因此,我们只是从可执行文件中借用字符串。
酷熊:“可执行文件”是指... ? (要问一个朋友)
Amos: target/debug/day1 中的文件是 Linux 上的 ELF、 Windows 上的 PE 和 macOS 上的 Mach-O,这是编译程序得到的结果(通过 cargo build 或 cargo run 构建,最终都会调用 rustc 编译器)。
酷熊:对!
因此,我们有 Split<&str>,且它实现了 Iterator<Item = &str>。
Iterator 是一个 trait,最重要的必需方法是 next():
fn next(&mut self) -> Option<Self::Item>
所以如果我们调用 s.next(),我们将得到一个 Some(a_slice),或者在元素耗尽时返回 None。
fn main() -> anyhow::Result<()> {
let mut s = include_str!("input.txt").split("\n");
dbg!(s.next());
dbg!(s.next());
dbg!(s.next());
Ok(())
}
$ cargo run --quiet
[src/main.rs:3] s.next() = Some(
"1470",
)
[src/main.rs:4] s.next() = Some(
"1577",
)
[src/main.rs:5] s.next() = Some(
"1054",
)
酷熊:等等,我收到一个警告:
Amos: 你遇到内联错误的警告?
酷熊:是的,这是 vscode 扩展 Error Lens,它相当整洁!
好吧,关于那个错误 —— 它不是一个真正的错误,它是一个来自 clippy 的诊断提示,这...我之前说过你应该安装 clippy 吧?
那么,你应该将它作为 rust-analyzer 默认的“检查保存”命令,将其保存在你的 VSCode 用户设置中:
{
"rust-analyzer.checkOnSave.command": "clippy",
}
clippy 试图告诉我们的是 '\n' 只是一个字符,这是一个很小的值,用其分割会被高度优化,而 "\n" 是一个字符串,可以是任意长度(只不过这里它的长度恰好是 1),所以我们强迫 .split 使用更通用(会导致更慢)的方法。
没有问题,我们可以很容易地修复,通过点击编辑器中的灯泡(💡)图标,或只是按下 Ctrl + .(MacOS 上可能是 Cmd + .):
然后我们得到下面的代码:
fn main() -> anyhow::Result<()> {
let mut s = include_str!("input.txt").split('\n');
dbg!(s.next());
dbg!(s.next());
dbg!(s.next());
Ok(())
}
执行结果和之前一样。
那么... 接下来我们要做什么呢? 让我们再看一下问题陈述:
具体来说,它们需要你找到两个总和为 2020 的条目,然后将这两个数字相乘。
对!就是描述的那样,不过有一个问题 —— 我们还没有数值,只有字符串。
幸运的是,str::parse 可以做到这一点!
那么,我们从一个迭代器 Iterator<Item = &str> 开始,我们应该需要 ... Iterator<Item = i64>?
小贴士
i64是一个有符号的 64 位整数——当我们不担心内存使用问题并且不确定能得到多大的数字时,使用它似乎是一个相对安全的选择。
对迭代器的所有项进行转换称为 mapping。由 Iterator trait 提供的 .map 方法可以实现 mapping。
我们可以直接传递一个函数: 在这里,我们传递 str::parse:
fn main() -> anyhow::Result<()> {
let mut s = include_str!("input.txt").split('\n').map(str::parse);
dbg!(s.next());
dbg!(s.next());
dbg!(s.next());
Ok(())
}
似乎不管用,报错了:
$ cargo run --quiet
error[E0283]: type annotations needed for `Map<std::str::Split<'_, char>, for<'r> fn(&'r str) -> std::result::Result<F, <F as FromStr>::Err> {core::str::<impl str>::parse::<F>}>`
--> src/main.rs:2:59
|
2 | let mut s = include_str!("input.txt").split('\n').map(str::parse);
| ----- ^^^^^^^^^^ cannot infer type for type parameter `F` declared on the associated function `parse`
| |
| consider giving `s` the explicit type `Map<std::str::Split<'_, char>, for<'r> fn(&'r str) -> std::result::Result<F, <F as FromStr>::Err> {core::str::<impl str>::parse::<F>}>`, where the type parameter `F` is specified
|
::: /home/amos/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/str/mod.rs:2202:21
|
2202 | pub fn parse<F: FromStr>(&self) -> Result<F, F::Err> {
| ------- required by this bound in `core::str::<impl str>::parse`
|
= note: cannot satisfy `_: FromStr`
help: consider specifying the type argument in the function call
|
2 | let mut s = include_str!("input.txt").split('\n').map(str::parse::<F>);
| ^^^^^
这绝对是个大错误,太不可思议了。
不过在最后还是有点帮助信息:
help: consider specifying the type argument in the function call
|
2 | let mut s = include_str!("input.txt").split('\n').map(str::parse::<F>);
| ^^^^^
你看,str::parse 可以把一个字符串解析成许多不同的东西 —— 我们可以解析一个 IP 地址,比如 127.0.0.1,或者我们可以解析成一个浮点值,比如 3.1415926535,诸如此类。
因此,借助我们的朋友 turbo-fish 的一点帮助,我们可以明确地说: 我们想要解析的是一个 i64 —— 一个有符号的 64 位整数:
fn main() -> anyhow::Result<()> {
let mut s = include_str!("input.txt").split('\n').map(str::parse::<i64>);
dbg!(s.next());
dbg!(s.next());
dbg!(s.next());
Ok(())
}
然后它就能正常执行了:
$ cargo run --quiet
[src/main.rs:3] s.next() = Some(
Ok(
1470,
),
)
[src/main.rs:4] s.next() = Some(
Ok(
1577,
),
)
[src/main.rs:5] s.next() = Some(
Ok(
1054,
),
)
我们所做的与下面的 JavaScript 代码类似,但并不完全相同:
["12", "34", "56"].map((x) => parseInt(x, 10))
酷熊小贴士 通过 JavaScript 代码可知:
parseInt接受多个参数,包括要解析的数字的基数(radix),因此我们不能只是将其传递给 map,后者将传递项和索引。 这个方法有效:
> ["12", "34", "56"].map((x) => parseInt(x, 10))
[ 12, 34, 56 ]
但下面这种方式就不行:
> ["12", "34", "56"].map(parseInt)
[ 12, NaN, NaN ]
它只对第一个(索引 0)和第十一个(索引 10)元素正常工作,否则它将尝试以 1 为基数、以 2 为基数、以 3 为基数等 11 个元素进行解析。 JavaScript 很有趣吧?
因为在 JavaScript 中,map 操作一个数组并返回一个数组。
但是在这里,我们使用迭代器: item 流。我们还不能随机访问他们所以我们必须打反复调用 .next() ,并且直到它们返回 None。
事实上,如果我们仔细观察我们的输出,我们会注意到我们每次打印的内容实际上是一个 Option<Result<i64, E>>
[src/main.rs:3] s.next() = Some(
Ok(
1470,
),
)
小贴士
Option<T>可以是 Some(T) ,也可以是None——iter.next()当没有item 时将返回None。Result<T, E>可以是Ok(T)或Err(E)—— 题目中输入的所有的行都是数字,所以我们只会得到Ok(i64)。
那么,我们怎样才能得到“一组数字”呢?
如果我们在 s.next() 的结果上调用 .unwrap() 的话,我们将从 Option<Result<i64, E>> 转换为 Result<i64, E>:
fn main() -> anyhow::Result<()> {
let mut s = include_str!("input.txt").split('\n').map(str::parse::<i64>);
dbg!(s.next().unwrap());
dbg!(s.next().unwrap());
dbg!(s.next().unwrap());
Ok(())
}
cargo run --quiet
[src/main.rs:5] s.next().unwrap() = Ok(
1470,
)
[src/main.rs:6] s.next().unwrap() = Ok(
1577,
)
[src/main.rs:7] s.next().unwrap() = Ok(
1054,
)
如果我们再次调用 .unwrap(),那么我们会得到单纯的 i64 值,如下所示:
fn main() -> anyhow::Result<()> {
let mut s = include_str!("input.txt").split('\n').map(str::parse::<i64>);
dbg!(s.next().unwrap().unwrap());
dbg!(s.next().unwrap().unwrap());
dbg!(s.next().unwrap().unwrap());
Ok(())
}
cargo run --quiet
[src/main.rs:3] s.next().unwrap().unwrap() = 1470
[src/main.rs:4] s.next().unwrap().unwrap() = 1577
[src/main.rs:5] s.next().unwrap().unwrap() = 1054
酷熊:就差一点! 但是... 这里到底发生了什么?
Amos: 当调用 Option<T>::unwrap() 要么 panic(如果它遇到了 None 变体),要么返回一个 T。
酷熊:panic,这不是很糟吗?
Amos: 没那么糟啦!在我们的程序中,如果其中一行不是数字,我们就不能做任何有用的事情,所以我们不妨安全地停止程序 —— 也就是恐慌(panic)。
酷熊:有道理,那第二次 unwrap() 呢?
Amos: 这个实际上是 Result<T, E>::unwrap() —— 它的工作原理也是类似。如果遇到 Err,它会 panic(携带错误类型 E 的格式化消息)。否则,它也返回 T。
酷熊:好吧。快速提问: 我们是否可以将 unwrap 传递给 map,这样我们就可以在从迭代器中检索所有项目时执行 unwrap?
Amos: 是的,我们可以! 让我们试试。
fn main() -> anyhow::Result<()> {
let mut s = include_str!("input.txt")
.split('\n')
.map(str::parse::<i64>)
.map(Result::unwrap);
dbg!(s.next().unwrap());
dbg!(s.next().unwrap());
dbg!(s.next().unwrap());
Ok(())
}
$ cargo run --quiet
[src/main.rs:6] s.next().unwrap() = 1470
[src/main.rs:7] s.next().unwrap() = 1577
[src/main.rs:8] s.next().unwrap() = 1054
酷熊:好的,那么...现在记笔记, s 是一个 Iterator<Item = i64>,对吗?
Amos: 对!
酷熊:那我们为什么还要 unwrap 呢?
Amos: 嗯,因为我们可以消耗完所有的 item —— 此时调用 .next() 仍然返回 Option<i64>。
酷熊:你能告诉我这时候会发生什么吗?
fn main() -> anyhow::Result<()> {
let mut s = include_str!("input.txt")
.split('\n')
.map(str::parse::<i64>)
.map(Result::unwrap)
.skip(198); // new!
dbg!(s.next().unwrap());
dbg!(s.next().unwrap());
dbg!(s.next().unwrap());
Ok(())
}
$ cargo run --quiet
[src/main.rs:8] s.next().unwrap() = 1282
[src/main.rs:9] s.next().unwrap() = 1306
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:10:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
酷熊:哦,对了,我们的 input.txt 文件只有 200 行,所以如果我们继续调用 .next()。在那之后,我们终于得到了 None。抓到你了。
我们先继续解题
酷熊:等等,抱歉打断你们。.如果我不想调用 Result::unwrap —— 如果我不想 panic,而是想返回一个简洁的错误,那该怎么办?
Amos: 我们过会儿再说这个,拉钩保证!
酷熊:好吧。
那么!问题是,我们需要在输入中找到一对数字,它们的总和是 2020,然后将它们相乘。
让我们尝试一些类型驱动的开发:
fn find_pair_whose_sum_is_2020(s: Vec<i64>) -> Option<(i64, i64)> {
todo!()
}
因此,给定一个 Vec<i64> —— 可以说就像我们今天所说的“array”一样接近,这个函数应该尝试找到两个和为 2020 的数字,并将它们作为元组返回,如: (2019,1)。
然而,它不是简单地返回一个 (i64,i64),因为这样的一对完全有可能不存在!如果我们的输入只是 vec![1, 3, 5],好吧,两个数字的组合不会等于 2020。
在实现这个函数之前,让我们尝试弄清楚如何使用它。
到目前为止,我们只有一个 Iterator<Item = i64>。但是这个函数希望随机访问集合中的任意项,所以它只能是 Vec<i64>。
Iterator::collect 可以帮助你完成工作。
main 函数如下:
fn main() -> anyhow::Result<()> {
let pair = find_pair_whose_sum_is_2020(
include_str!("input.txt")
.split('\n')
.map(str::parse::<i64>)
.map(Result::unwrap)
.collect(),
);
dbg!(pair);
Ok(())
}
fn find_pair_whose_sum_is_2020(s: Vec<i64>) -> Option<(i64, i64)> {
todo!()
}
编译后:
$ cargo check
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
但它不能运行:
$ cargo run --quiet
thread 'main' panicked at 'not yet implemented', src/main.rs:17:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
酷熊:哦,todo!() 是等待我们去实现的吗?
Amos: 对!它允许我们将某些内容标记为“待完成”,并且它将进行编译(而不是抱怨我们没有返回正确的类型,如果我们让 find_pair_whose_sum_is_2020 函数体直接为空,编译器会直接报错)
酷熊:太棒了,但结尾为何是个 !?
Amos: 因为它是宏,不是函数。
现在我们要做的就是实现这个函数!
Vec<i64> 可以视为一种数组,所以我们可以调用 .len(),用于返回它所拥有的条目数,类型是 usize,然后我们可以使用 [] 对它进行索引取值,就像在 JavaScript 中一样。
让我们试一试:
fn find_pair_whose_sum_is_2020(s: Vec<i64>) -> Option<(i64, i64)> {
for i in 0..s.len() {
for j in 0..s.len() {
if i == j {
continue;
}
if s[i] + s[j] == 2020 {
return Some((s[i], s[j]));
}
}
}
None
}
酷熊:这是怎么回事? 嵌套循环?
Amos: 是的,我们需要输入所有可能的数字对。
酷熊:continue 是因为...?
Amos: 因为如果我们的第一个项是 1010,我们另一个项是其本身,导致和为 2020。配对必须由不同的元素组成。
酷熊:原来如此。
上面这个解决方案真的有效!
$ cargo run --quiet
[src/main.rs:11] pair = Some(
(
376,
1644,
),
)
$ echo $((376 + 1644))
2020
酷熊:棒!可以改进吗?
Amos: 我想做一些改进。
首先,让我们实际上解决酷熊的合理要求“适当的错误处理”。
正如我们从 Iterator<Item = i64> 收集到的 Vec<i64> 一样,我们也可以将 Iterator<Item = Result<i64, E>> 收集到 Result<Vec<i64>, E> 中。
酷熊:所以这是一个不同的 collect 实现,它只是在出现第一个错误时停止?
Amos: 是的! 如果没有一个条目是 Err 变体,那么你将得到 Ok(some_vec),否则,将得到 Err(first_error)。
我们试试:
fn main() -> anyhow::Result<()> {
let pair = find_pair_whose_sum_is_2020(
include_str!("input.txt")
.split('\n')
.map(str::parse::<i64>)
.collect::<Result<Vec<_>, _>>()?,
);
dbg!(pair);
Ok(())
}
(这段代码具有完全相同的输出 —— 我就不展示 shell 会话的执行结果了。)
酷熊:不错! 还有,那有条大 turbofish。
在 collect() 之后为何还有 ??
Amos: 在某种意义上,它有点类似于 unwrap(),在那个场景中,它返回的是 Result<T, E> 和 “return”(实际上是 “evaluates to”)T。
酷熊:但它不会 panic 吗?
Amos: 它不会 panic,如果 Result 是 Err 变体,它返回一个 Err(E)。
酷熊:啊,所以它只能在返回 Result<T, E> 的函数中工作?
Amos: 对!
酷熊:但是我们的函数返回类型是 anyhow::Result<()>,哪里有 E?
Amos: 好吧,anyhow 是一个有助于错误处理的板条箱(crate)—— 它附带了一个错误类型,可以包含其他任何错误,真的。
因此,anyhow::Result 的定义实际上是:
pub type Result<T, E = Error> = core::result::Result<T, E>;
无论如何,这里的 Error 是 anyhow::Error。
酷熊:好的,那么 Result<()> 中的 () 是什么意思呢?
Amos: 空元组!
看看我们的 find_pair_whose_sum_is_2020 函数是如何返回 Option<(i64, i64)> 的。
酷熊:那类型 (),大小是多少..。
Amos: 大小为零,没有开销。
好了,我们的代码没问题,我想:
fn main() -> anyhow::Result<()> {
let pair = find_pair_whose_sum_is_2020(
include_str!("input.txt")
.split('\n')
.map(str::parse::<i64>)
.collect::<Result<Vec<_>, _>>()?,
);
dbg!(pair);
Ok(())
}
fn find_pair_whose_sum_is_2020(s: Vec<i64>) -> Option<(i64, i64)> {
for i in 0..s.len() {
for j in 0..s.len() {
if i == j {
continue;
}
if s[i] + s[j] == 2020 {
return Some((s[i], s[j]));
}
}
}
None
}
但我想给你们看更多的东西。
看,find_pair_whose_sum_is_2020 有点困扰我。你知道什么会很酷吗? 一个返回“结果对”的函数,像这样:
fn pairs(s: Vec<i64>) -> Vec<(i64, i64)> {
todo!()
}
酷熊:嗯。我们真的需要在这里使用 Vec<i64> 吗?其他集合类型呢?例如,[i64; 12]类型如果使用这种方法会不生效吗?固定大小的数组?
Amos: 有可能,我们可以使用切片(slice)替代。
fn all_pairs(s: &[i64]) -> Vec<(i64, i64)> {
todo!()
}
让我们真正实现它:
fn all_pairs(s: &[i64]) -> Vec<(i64, i64)> {
let mut pairs: Vec<_> = Default::default();
for i in 0..s.len() {
for j in 0..s.len() {
pairs.push((s[i], s[j]))
}
}
pairs
}
然后我们可以在 find_pair_whose_sum_is_2020 中使用它:
fn find_pair_whose_sum_is_2020(s: Vec<i64>) -> Option<(i64, i64)> {
for (a, b) in all_pairs(&s[..]) {
if a + b == 2020 {
return Some((a, b));
}
}
None
}
那个版本仍然正常执行!
酷熊:不错,不过我在想... 如果我们早点找到结果对(pair)呢? 在这种情况下,如果我们只使用前几个结果对,那么计算“所有的对”不是很浪费吗?
Amos: 没错,有什么办法解决这个问题吗?
酷熊:那么,我们可以在 all_pairs 中不返回 Vec<(i64, i64)>,而是返回... Iterator<Item = (i64, i64)>?
Amos: 可以! 事实上,你可以迭代一个 Vec,所以我们已经可以使用“迭代器风格”(iterator-style):
fn find_pair_whose_sum_is_2020(s: Vec<i64>) -> Option<(i64, i64)> {
all_pairs(&s[..]).into_iter().find(|(a, b)| a + b == 2020)
}
酷熊:哇哦!即使我们一开始就“找到”总和是 2020 的结果对,我们仍然在构建整个 Vec。
Amos: 没错,我们把这个也修复一下。
fn all_pairs(s: &[i64]) -> impl Iterator<Item = (i64, i64)> + '_ {
s.iter()
.copied()
.enumerate()
.map(move |(a_index, a)| {
s.iter()
.copied()
.enumerate()
.filter_map(move |(b_index, b)| {
if a_index == b_index {
None
} else {
Some((a, b))
}
})
})
.flatten()
}
酷熊:这... 有点粗糙。 我们能换种方式写吗?
Amos: 可以! 当然可以。或者我们仅仅使用 itertools crate 就能做到。
$ cargo add itertools
Adding itertools v0.9.0 to dependencies
实现变成如下所示:
use itertools::Itertools;
fn main() -> anyhow::Result<()> {
let pair = find_pair_whose_sum_is_2020(
include_str!("input.txt")
.split('\n')
.map(str::parse::<i64>)
.collect::<Result<Vec<_>, _>>()?,
);
dbg!(pair);
Ok(())
}
fn find_pair_whose_sum_is_2020(s: Vec<i64>) -> Option<(i64, i64)> {
s.into_iter()
.tuple_combinations()
.find(|(a, b)| a + b == 2020)
}
酷熊:哇,这样的话,我们是否有必要将 find_pair_whose_sum_is_2020 作为一个单独的函数?
Amos: 没有必要了。
use itertools::Itertools;
fn main() -> anyhow::Result<()> {
let (a, b) = include_str!("input.txt")
.split('\n')
.map(str::parse::<i64>)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.tuple_combinations()
.find(|(a, b)| a + b == 2020)
.expect("no pair had a sum of 2020");
dbg!(a + b);
dbg!(a * b);
Ok(())
}
$ cargo run --quiet
[src/main.rs:13] a + b = 2020
[src/main.rs:14] a * b = 618144
酷熊:太棒了!
我们为什么需要那个 .collect?我知道我们需要处理错误,但是我们不能只使用像 itertools::process_result 这样的工具吧?
Amos: 不能,因为 tuple_combinations 需要迭代器实现了 Clone。
酷熊:啊,因为它在输入中迭代好几次来组成组合..。
Amos: 你无法只用一次迭代来完成这件事,因为一旦你检索到一个值,它就会从迭代器中移除。
酷熊:第一部分结束了吗? Amos: 希望如此!
第二部分
我们看看问题陈述的第二部分:
在你的费用报告中,三项和为 2020 的所有组合有哪些?
酷熊:等等,等等,我可以做!
use itertools::Itertools;
fn main() -> anyhow::Result<()> {
let (a, b, c) = include_str!("input.txt")
.split('\n')
.map(str::parse::<i64>)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.tuple_combinations()
.find(|(a, b, c)| a + b + c == 2020)
.expect("no tuple of length 3 had a sum of 2020");
dbg!(a + b + c);
dbg!(a * b * c);
Ok(())
}
$ cargo run --quiet
[src/main.rs:13] a + b + c = 2020
[src/main.rs:14] a * b * c = 173538720
Amos: 干得好,小熊! 很幸运我们有 itertools 库。
酷熊:再见!
——完——