开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情
- Advent of Code 2020 Day5 译文(用 Rust 实现 Advent of Code 2020 第5天)
- 原文链接:fasterthanli.me/series/adve…
- 原文作者:Amos
- 译文来自:github.com/suhanyujie/…
- 译者:suhanyujie
- ps:水平有限,如有不当之处,欢迎指正
- 标签:Rust,advent of code, algo
又是一天 Advent of Code 2020
第 5 天,要实现什么呢?
酷熊:让我猜猜,还是解析?
Amos:对了!
有一家航空公司在提到座位时使用二分空间划分 —— 共有 128 排,8 列。前 7 个字符是 F (前面,表示下半部分)和 B (后面,表示上半部分) ,最后3个字符是 L (左边,表示下半部分)或 R (右边,表示上半部分)。
有如下例子,以 FBFBBFFRLR 为例:
- 首先考虑整个范围,从第 0 行到第 127 行(“行”即“排”)
- F 表示低位区域,即第 0 行到第 63 行
- B 代表选中区域的高位区域,是第 32 到 63 行
- F 表示代表选中区域的低位区域,第 32 行到第 47 行
- B 表示代表选中区域的高位区域,第 40 到 47 行
- B 代表选中区域的高位区域,即 44 到 47 行
- F 表示选中区域的低位区域,即第 44 到 45 行
- 最后一个 F 表示了两行中的低位行,第 44 行
后续的 RLR:
- 首先考虑整个范围,从第 0 到 7 列
- R 表示取上半部分,即第 4 到第 7 列
- L 表示选中区域中的下半部分,表示第 4 到第 5 列
- 最后一个 R 了选中区域中的高位列,即第5列
既然上次 peg 板条箱(crate)对我们很有用,我想尝试一个小小的挑战... 用一个简单的语法解决所有的问题。
酷熊:可以肯定的是,您不能使用解析器解决所有问题。
Amos:看我的!
$ cargo add peg
Adding peg v0.6.3 to dependencies
酷熊:等等!有点像二分法... 我们是使用 0..128 的范围吗,每次除以 2?
Amos:我有个不一样的方法。
实际上,问题描述的意思是 FBFBBF 只是一个字符串!F 表示 0,B 表示 1。
也就是说。。,等等,我又想到一个更好的主意!
$ cargo rm peg
Removing peg from dependencies
$ cargo add bitvec
Adding bitvec v0.19.4 to dependencies
酷熊:用 bitvec,你是认真的吗?
Amos:为什么不行? 虽然我也不喜欢位处理(位运算),这很让人困惑。
酷熊:所以... 你不知道怎么做?
Amos:我知道! 但是即使是专家,也经常会出错。
酷熊:你是说专家容易出错? 还是说你容易出错?
Amos:都有。
bitvec 真的很棒。它可以让你把任何东西当作 ... 位的矢量(vector)!这正是我们想要做的。
首先,行小于 255,且列小于 255,所以我们可以使用 u8:
#[derive(Default, Debug, PartialEq)]
struct Seat {
row: u8,
col: u8,
}
然后,嗯,然后我们对“位”做些处理:
use bitvec::prelude::*;
impl Seat {
const ROW_BITS: usize = 7;
const COL_BITS: usize = 3;
fn parse(input: &str) -> Self {
let bytes = input.as_bytes();
let mut res: Seat = Default::default();
{
// treat `res.row` as a collection of bits...
let row = BitSlice::<Msb0, _>::from_element_mut(&mut res.row);
// for each `F` or `B` element...
for (i, &b) in bytes[0..Self::ROW_BITS].iter().enumerate() {
// set the corresponding bit, in positions 1 through 7 (0-indexed)
row.set(
(8 - Self::ROW_BITS) + i,
match b {
b'F' => false,
b'B' => true,
_ => panic!("unexpected row letter: {}", b as char),
},
);
}
}
{
let col = BitSlice::<Msb0, _>::from_element_mut(&mut res.col);
for (i, &b) in bytes[Self::ROW_BITS..][..Self::COL_BITS].iter().enumerate() {
col.set(
(8 - Self::COL_BITS) + i,
match b {
b'L' => false,
b'R' => true,
_ => panic!("unexpected col letter: {}", b as char),
},
);
}
}
res
}
}
酷熊:真厉害,能有用吗?
#[test]
fn test_parse() {
let input = "FBFBBFFRLR";
let seat = Seat::parse(input);
assert_eq!(seat, Seat { row: 44, col: 5 });
}
$ cargo test --quiet
running 1 test
.
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Amos:看起来有用!
接下来,我们必须根据行和列计算座位 ID。我们可以为此实现一个方法。
问题描述说要把行数乘以 8 但是... 我们看穿了他们的把戏。这次我们可以稍微改变一下:
impl Seat {
fn id(&self) -> u64 {
((self.row as u64) << Self::COL_BITS) + (self.col as u64)
}
}
酷熊:但是... 题目要求乘以 8 的。
Amos:是的,但是概念上... 不一样。
我们可以用问题描述中给出的例子进行测试:
#[test]
fn test_seat_id() {
macro_rules! validate {
($input: expr, $row: expr, $col: expr, $id: expr) => {
let seat = Seat::parse($input);
assert_eq!(
seat,
Seat {
row: $row,
col: $col
}
);
assert_eq!(seat.id(), $id);
};
}
validate!("BFFFBBFRRR", 70, 7, 567);
validate!("FFFBBBFRRR", 14, 7, 119);
validate!("BBFFBBFRLL", 102, 4, 820);
}
是时候回答问题了:在我们的输入中,最高的座位 ID 是什么?
itertools 库中有一个基于迭代器的 max 函数,我喜欢用它!
$ cargo add itertools
Adding itertools v0.9.0 to dependencies
fn main() {
let max_id = itertools::max(
include_str!("input.txt")
.lines()
.map(Seat::parse)
.map(|seat| seat.id()),
);
println!("The maximum seat ID is {:?}", max_id);
}
$ cargo run --quiet
The maximum seat ID is Some(885)
酷熊:还不错,挺快的。
第二部分
第二部分题目是从名单中找到一个缺失的座位!但它不在飞机的最前面,也不在最后面,因为那些座位实际上并不存在。介于两者之间。
所以我们能做的就是。。
酷熊:Amos 等等
Amos:怎么了?
酷熊:我们就不能简化一下吗?
Amos:什么意思?
酷熊:听我说: 我们的 Seat 类型只是一个 u16。当你仔细想想的时候,你就会觉得这合理吗?技术上来说,行是 u10 类型 —— 7 个比特位,列是3 个比特位。
Amos:继续说。
酷熊:然后我们一次解析 10 个位 —— 我们用行和列的 getter 来代替!
Amos:这样也行。
酷熊:如果我们没有行和列的 getter,因为没有其他额外的限制要求。也许还有更好的方式,我教你:
use bitvec::prelude::*;
#[derive(Clone, Copy, Default, Debug, PartialEq)]
struct Seat(u16);
impl Seat {
fn parse(input: &str) -> Self {
let mut res: Seat = Default::default();
let bits = BitSlice::<Lsb0, _>::from_element_mut(&mut res.0);
for (i, &b) in input.as_bytes().iter().rev().enumerate() {
bits.set(
i,
match b {
b'F' | b'L' => false,
b'B' | b'R' => true,
_ => panic!("unexpected letter: {}", b as char),
},
)
}
res
}
}
#[test]
fn test_seat_id() {
assert_eq!(Seat::parse("BFFFBBFRRR"), Seat(567));
assert_eq!(Seat::parse("FFFBBBFRRR"), Seat(119));
assert_eq!(Seat::parse("BBFFBBFRLL"), Seat(820));
}
fn main() {
let max_id = itertools::max(
include_str!("input.txt")
.lines()
.map(Seat::parse)
.map(|seat| seat.0),
);
println!("The maximum seat ID is {:?}", max_id);
}
$ cargo t -q
running 1 test
.
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
$ cargo r -q
The maximum seat ID is Some(885)
Amos:真不错!你知道,在某些文化中,一次性重构同事的全部代码是不礼貌的。
酷熊:你说什么呢?我甚至反转了迭代器,使用 Lsb0(最小有效位优先)顺序,这样我们就不必担心算术了!
Amos:我得承认... 它更简短。
酷熊:想想我刚刚在推特上救了你。
现在,回答第二部分的问题。这里有一个想法: 我们如何收集所有的 ID,对它们进行排序(从最小到最大) ,然后迭代,跟踪最后一个 ID,当间隔大于 1 时 —— 就是它!我们找到座位了。
首先,为了能够对 Vec<Seat> 进行排序,我们需要给我们的类型 derive Ord - 来表明(仅仅是一个“穿着风衣”的 u16)它可以排序。
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Seat(u16);
然后,我们只需要做我们想做的事。对于第一个迭代,我们不会得到“last id”,所以使用 Option 包装:
fn main() {
let mut ids: Vec<_> = include_str!("input.txt").lines().map(Seat::parse).collect();
ids.sort();
let mut last_id: Option<Seat> = None;
for id in ids {
if let Some(last_id) = last_id {
let gap = id.0 - last_id.0;
if gap > 1 {
println!("Our seat ID is {}", last_id.0 + 1);
return;
}
}
last_id = Some(id);
}
}
就是这样
$ cargo run --quiet
Our seat ID is 623
我们又解开了一个谜题!
下次见!