advent-of-code/2023/src/bin/day08.rs

104 lines
2.8 KiB
Rust
Raw Normal View History

2023-12-08 06:30:11 +01:00
#![feature(test)]
extern crate test;
2023-12-08 10:40:04 +01:00
use std::hint::unreachable_unchecked;
2023-12-08 06:30:11 +01:00
use aoc2023::{boilerplate, common::*};
2023-12-08 10:06:19 +01:00
const DAY: usize = 8;
2023-12-08 10:40:04 +01:00
type Parsed<'a> = (Vec<Direction>, Vec<u16>, [[u16; 2]; LUT_SIZE]);
2023-12-08 06:30:11 +01:00
2023-12-08 07:05:56 +01:00
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)]
2023-12-08 06:30:11 +01:00
enum Direction {
2023-12-08 10:06:19 +01:00
Left = 0,
Right = 1,
2023-12-08 06:30:11 +01:00
}
2023-12-08 10:40:04 +01:00
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))
}
2023-12-08 06:30:11 +01:00
fn parse_input(raw: &str) -> Parsed {
let (directions, map) = raw.split_once("\n\n").unwrap();
let directions = directions
.bytes()
.map(|i| match i {
b'L' => Direction::Left,
b'R' => Direction::Right,
2023-12-08 10:40:04 +01:00
_ => unsafe { unreachable_unchecked() },
2023-12-08 06:30:11 +01:00
})
.collect();
2023-12-08 10:40:04 +01:00
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)
}
#[inline]
fn ends_with(packed: u16, suffix: u8) -> bool {
packed & 0b11111 == suffix as u16 & 0b11111
2023-12-08 06:30:11 +01:00
}
2023-12-08 10:40:04 +01:00
fn steps_until((directions, _, map): &Parsed, start: u16) -> usize {
2023-12-08 06:30:11 +01:00
directions
.iter()
.cycle()
2023-12-08 07:05:56 +01:00
.scan(start, |pos, dir| {
2023-12-08 10:40:04 +01:00
let next = map[*pos as usize][*dir as usize];
(!ends_with(next, b'Z')).then(|| *pos = next)
2023-12-08 06:30:11 +01:00
})
.count()
+ 1
}
2023-12-08 08:54:41 +01:00
fn part1(parsed: &Parsed) -> usize {
2023-12-08 10:40:04 +01:00
steps_until(parsed, pack(&[b'A'; 3]))
2023-12-08 08:54:41 +01:00
}
2023-12-08 07:05:56 +01:00
// I’m honestly not sure why this works. It seems each path only has a single ghost node, and that
// node occurs right before looping, so we can just compute the least common multiple of their step counts.
// 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.
2023-12-08 06:30:11 +01:00
fn part2(parsed: &Parsed) -> usize {
2023-12-08 10:40:04 +01:00
parsed.1.iter().filter(|&&n| ends_with(n, b'A')).fold(1, |acc, n| lcm(acc, steps_until(parsed, *n)))
2023-12-08 06:30:11 +01:00
}
2023-12-08 07:05:56 +01:00
#[cfg(test)]
const TEST_INPUT_P2: &str = "LR
11A = (11B, XXX)
11B = (XXX, 11Z)
11Z = (11B, XXX)
22A = (22B, XXX)
22B = (22C, 22C)
22C = (22Z, 22Z)
22Z = (22B, 22B)
XXX = (XXX, XXX)";
2023-12-08 06:30:11 +01:00
boilerplate! {
TEST_INPUT == "LLR
AAA = (BBB, BBB)
BBB = (AAA, ZZZ)
ZZZ = (ZZZ, ZZZ)",
tests: {
part1: { TEST_INPUT => 6 },
2023-12-08 07:05:56 +01:00
part2: { TEST_INPUT_P2 => 6 },
2023-12-08 06:30:11 +01:00
},
bench1 == 12083,
2023-12-08 07:05:56 +01:00
bench2 == 13385272668829,
2023-12-08 10:40:04 +01:00
bench_parse: |(v, v2, m): &Parsed| { assert_eq!(m[0], [0, 0]); (v.len(), v2.len(), v[0]) } => (281, 6, Direction::Left),
2023-12-08 06:30:11 +01:00
}