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