aoc/year2022/day03.rs
1//! # Rucksack Reorganization
2//!
3//! The core idea of this puzzle is computing set intersection. We could use the built-in `HashSet`
4//! but as the cardinality of the set is so small (52 maximum including both lowercase and upper
5//! case letters) we can instead use a much faster approach of storing each set in a single `u128`
6//! integer and using bit manipulation.
7//!
8//! If a letter is present in the set then the corresponding bit will be `1` otherwise `0`.
9//! For example to add the letter "a", logical OR the set with 1 shifted left by 97
10//!
11//! `set | (1 << 97)`
12//!
13//! Set intersection is the logical AND of two integers which compiles to a single machine instruction.
14//!
15//! `a & b`
16//!
17//! To obtain the score we can use the [`trailing_zeroes`] method to find the first set bit. On most
18//! architectures this also compiles down to a single instruction (`LZCNT` on x86 or `CLZ` on ARM)
19//! that is blazing fast.
20//!
21//! Notes:
22//! * We could get away with a `u64` for the set, but by using an `u128` we can shift directly by the
23//! raw ASCII codes and not bother computing offsets until the very end.
24//!
25//! [`trailing_zeroes`]: u128
26use crate::util::iter::*;
27
28/// Collect each line into a `vec` of string slices.
29pub fn parse(input: &str) -> Vec<&str> {
30 input.lines().collect()
31}
32
33/// Split each line into 2 equal halves, then compute the set intersection.
34pub fn part1(input: &[&str]) -> u32 {
35 input
36 .iter()
37 .map(|rucksack| {
38 let (a, b) = rucksack.split_at(rucksack.len() / 2);
39 priority(mask(a) & mask(b))
40 })
41 .sum()
42}
43
44/// Group lines into chunks of 3, then compute the mutual set intersection.
45pub fn part2(input: &[&str]) -> u32 {
46 input.iter().chunk::<3>().map(|[a, b, c]| priority(mask(a) & mask(b) & mask(c))).sum()
47}
48
49/// Build a set from a slice of ASCII characters, using the `fold` function to repeatedly OR
50/// bit offsets into an accumulator.
51fn mask(s: &str) -> u128 {
52 s.bytes().fold(0, |acc, b| acc | (1 << b))
53}
54
55/// Find the lowest set bit (there should only be one) then convert to priority using the
56/// given rules.
57fn priority(mask: u128) -> u32 {
58 let zeroes = mask.trailing_zeros();
59 match zeroes {
60 65..=90 => zeroes - 38,
61 97..=122 => zeroes - 96,
62 _ => unreachable!(),
63 }
64}