diff --git a/2023/src/bin/day08.rs b/2023/src/bin/day08.rs index daa0105..ace2368 100644 --- a/2023/src/bin/day08.rs +++ b/2023/src/bin/day08.rs @@ -1,11 +1,12 @@ #![feature(test)] extern crate test; +use std::hint::unreachable_unchecked; + use aoc2023::{boilerplate, common::*}; -use fnv::FnvHashMap as HashMap; const DAY: usize = 8; -type Parsed<'a> = (Vec, HashMap<&'a str, [&'a str; 2]>); +type Parsed<'a> = (Vec, Vec, [[u16; 2]; LUT_SIZE]); #[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] enum Direction { @@ -13,6 +14,12 @@ enum Direction { Right = 1, } +const LUT_SIZE: usize = 27483; // pack("ZZZ") + 1 + +fn pack(s: &[u8; 3]) -> u16 { + s.iter().cloned().fold(0, |acc, n| (acc << 5) + (n as u16 & 0b11111)) +} + fn parse_input(raw: &str) -> Parsed { let (directions, map) = raw.split_once("\n\n").unwrap(); let directions = directions @@ -20,27 +27,44 @@ fn parse_input(raw: &str) -> Parsed { .map(|i| match i { b'L' => Direction::Left, b'R' => Direction::Right, - _ => unreachable!(), + _ => unsafe { unreachable_unchecked() }, }) .collect(); - let map = map.lines().map(|l| (&l[0..=2], [&l[7..=9], &l[12..=14]])).collect(); - (directions, map) + let mut lut = [[0u16, 0]; LUT_SIZE]; + let mut indices = Vec::new(); + for x in map.lines().map(|l| l.as_bytes()) { + unsafe { + let idx = pack(&*x[0..=2].as_ptr().cast()); + let left = pack(&*x[7..=9].as_ptr().cast()); + let right = pack(&*x[12..=14].as_ptr().cast()); + lut[idx as usize] = [left, right]; + if x[2] == b'A' { + indices.push(idx); + } + } + } + (directions, indices, lut) } -fn steps_until((directions, map): &Parsed, start: &str, target: &str) -> usize { +#[inline] +fn ends_with(packed: u16, suffix: u8) -> bool { + packed & 0b11111 == suffix as u16 & 0b11111 +} + +fn steps_until((directions, _, map): &Parsed, start: u16) -> usize { directions .iter() .cycle() .scan(start, |pos, dir| { - let next = map.get(pos)?[*dir as usize]; - (!next.ends_with(target)).then(|| *pos = next) + let next = map[*pos as usize][*dir as usize]; + (!ends_with(next, b'Z')).then(|| *pos = next) }) .count() + 1 } fn part1(parsed: &Parsed) -> usize { - steps_until(parsed, "AAA", "ZZZ") + steps_until(parsed, pack(&[b'A'; 3])) } // I’m honestly not sure why this works. It seems each path only has a single ghost node, and that @@ -48,7 +72,7 @@ fn part1(parsed: &Parsed) -> usize { // I assume this holds true for other inputs (it can’t have been random), // but I don’t see it anywhere in the task and only found out by experimentation. fn part2(parsed: &Parsed) -> usize { - parsed.1.keys().filter(|start| start.ends_with("A")).fold(1, |acc, n| lcm(acc, steps_until(parsed, n, "Z"))) + parsed.1.iter().filter(|&&n| ends_with(n, b'A')).fold(1, |acc, n| lcm(acc, steps_until(parsed, *n))) } #[cfg(test)] @@ -75,5 +99,5 @@ ZZZ = (ZZZ, ZZZ)", }, bench1 == 12083, bench2 == 13385272668829, - bench_parse: |(v, m): &Parsed| { assert_eq!(m["AAA"], ["MJJ", "QBJ"]); (v.len(), v[0]) } => (281, Direction::Left), + bench_parse: |(v, v2, m): &Parsed| { assert_eq!(m[0], [0, 0]); (v.len(), v2.len(), v[0]) } => (281, 6, Direction::Left), }