【译】用 Rust 实现 Advent of Code 2020 第5天

160 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情

又是一天 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

我们又解开了一个谜题!

下次见!