aoc/year2024/day17.rs
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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
//! # Chronospatial Computer
//!
//! Part one implements the computer specification then runs the provided program. The `b` and `c`
//! registers are assumed to be always zero in the provided input. The computer uses a resumable
//! `run` method that returns `Some(out)` to indicate output and `None` to indicate program end.
//! This is the same flexible approach used by the 2019 [`Intcode`] computer.
//!
//! For part two, reverse engineering the assembly shows that it implements the following
//! algorithm:
//!
//! ```none
//! while a != 0 {
//! b = // Some hash based on value of a
//! out b
//! a >>= 3
//! }
//! ```
//!
//! This means that the final value of `a` must be zero. Starting with this knowledge we work
//! backwards digit by digit. The right shift wipes out the lowest 3 bits of `a` so there could
//! be 8 possible previous values. We check each possible value recursively, exploring only
//! those that result in the correct program digit.
//!
//! For each new item we check each of the 8 possible combinations against the next digit
//! in reverse, and so on until we have all possible valid starting values of `a`.
//!
//! Although it may seem that checking could grow exponentially to 8¹⁶ potential values,
//! in practice filtering by correct digit keeps the total less than 50.
//!
//! [`Intcode`]: crate::year2019::intcode
use crate::util::parse::*;
use std::ops::ControlFlow;
pub fn parse(input: &str) -> Vec<u64> {
input.iter_unsigned().collect()
}
pub fn part1(input: &[u64]) -> String {
// We only care about the value of `a`.
let mut computer = Computer::new(input, input[0]);
let mut out = Vec::new();
while let Some(n) = computer.run() {
let digit = (n as u8 + b'0') as char;
out.push(digit);
out.push(',');
}
out.pop();
out.iter().collect()
}
pub fn part2(input: &[u64]) -> u64 {
// Start with known final value of `a`.
helper(input, input.len() - 1, 0).break_value().unwrap()
}
fn helper(program: &[u64], index: usize, a: u64) -> ControlFlow<u64> {
if index == 2 {
return ControlFlow::Break(a);
}
// Try all 8 combination of lower 3 bits.
for i in 0..8 {
let next_a = (a << 3) | i;
let out = Computer::new(program, next_a).run().unwrap();
if out == program[index] {
helper(program, index - 1, next_a)?;
}
}
ControlFlow::Continue(())
}
struct Computer<'a> {
program: &'a [u64],
a: u64,
b: u64,
c: u64,
ip: usize,
}
impl Computer<'_> {
/// The values of `b` and `c` are always 0 in the provided inputs.
fn new(input: &[u64], a: u64) -> Computer<'_> {
Computer { program: &input[3..], a, b: 0, c: 0, ip: 0 }
}
fn run(&mut self) -> Option<u64> {
while self.ip < self.program.len() {
// Convenience closures.
let literal = || self.program[self.ip + 1];
let combo = || match self.program[self.ip + 1] {
n @ 0..4 => n,
4 => self.a,
5 => self.b,
6 => self.c,
_ => unreachable!(),
};
// Computer specification.
match self.program[self.ip] {
0 => self.a >>= combo(),
1 => self.b ^= literal(),
2 => self.b = combo() % 8,
3 => {
if self.a != 0 {
self.ip = literal() as usize;
continue;
}
}
4 => self.b ^= self.c,
5 => {
let out = combo() % 8;
self.ip += 2;
return Some(out);
}
6 => self.b = self.a >> combo(),
7 => self.c = self.a >> combo(),
_ => unreachable!(),
}
self.ip += 2;
}
None
}
}