diff --git a/2021/src/common.rs b/2021/src/common.rs index 487b8eb..600cd04 100644 --- a/2021/src/common.rs +++ b/2021/src/common.rs @@ -90,7 +90,14 @@ where let mut buffer = Vec::new(); input.read_to_end(&mut buffer).unwrap(); - let (_, output) = parser(&buffer).finish().unwrap(); - - output + match parser(&buffer).finish() { + Ok((_, output)) => output, + Err(err) => { + panic!( + "Failed to parse input with error {:?} at \"{}\"", + err.code, + String::from_utf8_lossy(err.input) + ); + } + } } diff --git a/2021/src/day24.rs b/2021/src/day24.rs index 113ba49..6646653 100644 --- a/2021/src/day24.rs +++ b/2021/src/day24.rs @@ -1,9 +1,159 @@ +//! Very input-specific reverse-engineered solution +//! +//! # General implementation +//! +//! The code in the examples is a series of 14 times this: +//! +//! ```txt +//! inp w -> read digit +//! mul x 0 +//! add x z +//! mod x 26 -> x = z % 26 +//! div z $A -> pop Z (see below) +//! add x $B +//! eql x w -> x = ((z + $B) == w) +//! eql x 0 -> x = ((z + $B) != w) +//! mul y 0 +//! add y 25 +//! mul y x +//! add y 1 -> if x { 26 } else { 1 } +//! mul z y -> if x { z *= 26 } (push z, see below) +//! mul y 0 +//! add y w +//! add y $C -> y = w + $C +//! mul y x +//! add z y -> if x { z += w + $C } +//! ``` +//! +//! `$A` is either `1` or `26` which we can translate to a bool `$A == 26` for convenience. This +//! simplifies to the following rust. +//! +//! ``` +//! fn validate(mut z: i32, digit: i32) -> i32 { +//! let x = (z % 26 + B) != digit; +//! if A { +//! z /= 26; +//! } +//! +//! if x { +//! z = 26 * z + digit + C; +//! } +//! +//! z +//! } +//! ``` +//! +//! In human terms, `z` is used to hold a base 26 number. When `$A` is `true`, we pop off the least +//! significant digit by dividing by 26. Then, depending on whether `(z + $B) % 26` is equal to our +//! digit, we push `digit + $C`. Ideally, we should pop as often as we push in order to arrive at `z +//! == 0` in the end. The input contains 7 pops, so we want each of those to not push. +//! +//! To solve this problem, we use a backtracking memoizing algorithm, where we cancel every sequence +//! that fails to pop at some point. A pop is failed whenever we execute a validation pop where we +//! can pop if `x` happened to be set to `0` at the time of the check. We can memoize this over the +//! running value of `z`. +//! +//! This implementation probably doesn't work on other people's input; to fix it, you'll want to +//! update the `parse_step` function. Good luck with that. +use std::collections::HashMap; use std::io::Read; -pub fn part1(_input: &mut dyn Read) -> String { - todo!() +use nom::branch::alt; +use nom::bytes::complete::tag; +use nom::character::complete::newline; +use nom::combinator::map; +use nom::multi::separated_list1; +use nom::sequence::delimited; +use nom::sequence::preceded; +use nom::sequence::tuple; +use nom::IResult; + +use crate::common::read_input; + +type Cache = HashMap<(usize, i32), Option>; + +#[derive(Debug)] +struct Step(bool, i32, i32); + +impl Step { + fn evaluate(&self, digit: i32, z: i32) -> Option { + if self.0 { + (z % 26 + self.1 == digit).then(|| z / 26) + } else { + Some(z * 26 + digit + self.2) + } + } } -pub fn part2(_input: &mut dyn Read) -> String { - todo!() +fn parse_step(input: &[u8]) -> IResult<&[u8], Step> { + use nom::character::complete::i32; + + let parse_pop = preceded( + tag("inp w\nmul x 0\nadd x z\nmod x 26\ndiv z "), + alt((map(tag("1"), |_| false), map(tag("26"), |_| true))), + ); + + let parse_a = preceded(tag("\nadd x "), i32); + + let parse_b = delimited( + tag("\neql x w\neql x 0\nmul y 0\nadd y 25\nmul y x\nadd y 1\nmul z y\nmul y 0\nadd y w\nadd y "), + i32, + tag("\nmul y x\nadd z y"), + ); + + map(tuple((parse_pop, parse_a, parse_b)), |(pop, a, b)| { + Step(pop, a, b) + })(input) +} + +fn parse_input(input: &[u8]) -> IResult<&[u8], Vec> { + separated_list1(newline, parse_step)(input) +} + +fn optimize( + current: usize, + steps: &[Step], + digits: &[i32], + z: i32, + cache: &mut Cache, +) -> Option { + if current >= steps.len() { + return (z == 0).then(|| 0); + } + + if let Some(&memoized) = cache.get(&(current, z)) { + return memoized; + } + + let result = digits.iter().find_map(|&digit| { + let z = steps[current].evaluate(digit, z)?; + let result = optimize(current + 1, steps, digits, z, cache)?; + + Some(result + digit as i64 * 10i64.pow(13 - current as u32)) + }); + cache.insert((current, z), result); + + result +} + +fn parts_common(input: &mut dyn Read, digits: &[i32]) -> String { + let steps = read_input(input, parse_input); + + let mut cache = Cache::new(); + + optimize(0, &steps, digits, 0, &mut cache) + .unwrap() + .to_string() +} + +pub fn part1(input: &mut dyn Read) -> String { + let digits = [9, 8, 7, 6, 5, 4, 3, 2, 1]; + + parts_common(input, &digits) +} + +pub fn part2(input: &mut dyn Read) -> String { + let digits = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + + parts_common(input, &digits) }