Add 2024 Day 17

This commit is contained in:
Burnus 2024-12-17 16:42:09 +01:00
parent a4460964fb
commit bb8fc8c5f0
5 changed files with 295 additions and 0 deletions

View file

@ -0,0 +1,13 @@
[package]
name = "day17_chronospatial_computer"
version = "0.1.0"
edition = "2021"
[dependencies]
[dev-dependencies]
criterion = "0.5.1"
[[bench]]
name = "test_benchmark"
harness = false

View file

@ -0,0 +1,92 @@
The Historians push the button on their strange device, but this time, you all just feel like you're [falling](/2018/day/6).
"Situation critical", the device announces in a familiar voice. "Bootstrapping process failed. Initializing debugger...."
The small handheld device suddenly unfolds into an entire computer! The Historians look around nervously before one of them tosses it to you.
This seems to be a 3-bit computer: its program is a list of 3-bit numbers (0 through 7), like `0,1,2,3`. The computer also has three *registers* named `A`, `B`, and `C`, but these registers aren't limited to 3 bits and can instead hold any integer.
The computer knows *eight instructions*, each identified by a 3-bit number (called the instruction's *opcode*). Each instruction also reads the 3-bit number after it as an input; this is called its *operand*.
A number called the *instruction pointer* identifies the position in the program from which the next opcode will be read; it starts at `0`, pointing at the first 3-bit number in the program. Except for jump instructions, the instruction pointer increases by `2` after each instruction is processed (to move past the instruction's opcode and its operand). If the computer tries to read an opcode past the end of the program, it instead *halts*.
So, the program `0,1,2,3` would run the instruction whose opcode is `0` and pass it the operand `1`, then run the instruction having opcode `2` and pass it the operand `3`, then halt.
There are two types of operands; each instruction specifies the type of its operand. The value of a *literal operand* is the operand itself. For example, the value of the literal operand `7` is the number `7`. The value of a *combo operand* can be found as follows:
* Combo operands `0` through `3` represent literal values `0` through `3`.
* Combo operand `4` represents the value of register `A`.
* Combo operand `5` represents the value of register `B`.
* Combo operand `6` represents the value of register `C`.
* Combo operand `7` is reserved and will not appear in valid programs.
The eight instructions are as follows:
The `*adv*` instruction (opcode `*0*`) performs *division*. The numerator is the value in the `A` register. The denominator is found by raising 2 to the power of the instruction's *combo* operand. (So, an operand of `2` would divide `A` by `4` (`2^2`); an operand of `5` would divide `A` by `2^B`.) The result of the division operation is *truncated* to an integer and then written to the `A` register.
The `*bxl*` instruction (opcode `*1*`) calculates the [bitwise XOR](https://en.wikipedia.org/wiki/Bitwise_operation#XOR) of register `B` and the instruction's *literal* operand, then stores the result in register `B`.
The `*bst*` instruction (opcode `*2*`) calculates the value of its *combo* operand [modulo](https://en.wikipedia.org/wiki/Modulo) 8 (thereby keeping only its lowest 3 bits), then writes that value to the `B` register.
The `*jnz*` instruction (opcode `*3*`) does *nothing* if the `A` register is `0`. However, if the `A` register is *not zero*, it *jumps* by setting the instruction pointer to the value of its *literal* operand; if this instruction jumps, the instruction pointer is *not* increased by `2` after this instruction.
The `*bxc*` instruction (opcode `*4*`) calculates the *bitwise XOR* of register `B` and register `C`, then stores the result in register `B`. (For legacy reasons, this instruction reads an operand but *ignores* it.)
The `*out*` instruction (opcode `*5*`) calculates the value of its *combo* operand modulo 8, then *outputs* that value. (If a program outputs multiple values, they are separated by commas.)
The `*bdv*` instruction (opcode `*6*`) works exactly like the `adv` instruction except that the result is stored in the *`B` register*. (The numerator is still read from the `A` register.)
The `*cdv*` instruction (opcode `*7*`) works exactly like the `adv` instruction except that the result is stored in the *`C` register*. (The numerator is still read from the `A` register.)
Here are some examples of instruction operation:
* If register `C` contains `9`, the program `2,6` would set register `B` to `1`.
* If register `A` contains `10`, the program `5,0,5,1,5,4` would output `0,1,2`.
* If register `A` contains `2024`, the program `0,1,5,4,3,0` would output `4,2,5,6,7,7,7,7,3,1,0` and leave `0` in register `A`.
* If register `B` contains `29`, the program `1,7` would set register `B` to `26`.
* If register `B` contains `2024` and register `C` contains `43690`, the program `4,0` would set register `B` to `44354`.
The Historians' strange device has finished initializing its debugger and is displaying some *information about the program it is trying to run* (your puzzle input). For example:
```
Register A: 729
Register B: 0
Register C: 0
Program: 0,1,5,4,3,0
```
Your first task is to *determine what the program is trying to output*. To do this, initialize the registers to the given values, then run the given program, collecting any output produced by `out` instructions. (Always join the values produced by `out` instructions with commas.) After the above program halts, its final output will be `*4,6,3,5,6,3,5,2,1,0*`.
Using the information provided by the debugger, initialize the registers to the given values, then run the program. Once it halts, *what do you get if you use commas to join the values it output into a single string?*
Your puzzle answer was `7,4,2,5,1,4,6,0,4`.
\--- Part Two ---
----------
Digging deeper in the device's manual, you discover the problem: this program is supposed to *output another copy of the program*! Unfortunately, the value in register `A` seems to have been corrupted. You'll need to find a new value to which you can initialize register `A` so that the program's output instructions produce an exact copy of the program itself.
For example:
```
Register A: 2024
Register B: 0
Register C: 0
Program: 0,3,5,4,3,0
```
This program outputs a copy of itself if register `A` is instead initialized to `*117440*`. (The original initial value of register `A`, `2024`, is ignored.)
*What is the lowest positive initial value for register `A` that causes the program to output a copy of itself?*
Your puzzle answer was `164278764924605`.
Both parts of this puzzle are complete! They provide two gold stars: \*\*
At this point, you should [return to your Advent calendar](/2024) and try another puzzle.
If you still want to see it, you can [get your puzzle input](17/input).

View file

@ -0,0 +1,180 @@
use core::fmt::Display;
use std::num::ParseIntError;
#[derive(Debug, PartialEq, Eq)]
pub enum ParseError {
ParseIntError(std::num::ParseIntError),
LineCount,
ProgramNotSet,
RegisterNotSet(u8),
}
impl From<ParseIntError> for ParseError {
fn from(value: ParseIntError) -> Self {
Self::ParseIntError(value)
}
}
impl Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LineCount => write!(f, "Input must consist of 5 lines: One setting each register, an empty line, and the program"),
Self::ParseIntError(e) => write!(f, "Unable to parse into integer: {e}"),
Self::ProgramNotSet => write!(f, "Line 5 must contain the program at its last position (separated by whitespace)"),
Self::RegisterNotSet(n) => write!(f, "Line {n} must contain the value for register {} at its last position (separated by whitespace)", b'@' + n),
}
}
}
#[derive(Default, Clone)]
struct Computer {
opcode: Vec<u8>,
instruction_ptr: usize,
registers: [usize; 3],
output: Vec<u8>,
}
impl TryFrom<&str> for Computer {
type Error = ParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let lines: Vec<_> = value.lines().collect();
if lines.len() != 5 {
return Err(Self::Error::LineCount);
}
let a = lines[0].split_whitespace().last().ok_or(Self::Error::RegisterNotSet(1))?.parse::<usize>()?;
let b = lines[1].split_whitespace().last().ok_or(Self::Error::RegisterNotSet(2))?.parse::<usize>()?;
let c = lines[2].split_whitespace().last().ok_or(Self::Error::RegisterNotSet(3))?.parse::<usize>()?;
let registers = [a, b, c];
let opcode = lines[4]
.split_whitespace()
.last()
.ok_or(Self::Error::ProgramNotSet)?
.split(',')
.map(|n| n.parse::<u8>())
.collect::<Result<Vec<_>, _>>()?;
Ok(Self {
opcode,
registers,
..Default::default()
})
}
}
impl Computer {
fn combo(&self, operand: usize) -> usize {
if operand < 4 {
operand
} else {
self.registers[operand - 4]
}
}
fn adv(&mut self, operand: usize) {
self.registers[0] >>= self.combo(operand);
}
fn bxl(&mut self, operand: usize) {
self.registers[1] ^= operand
}
fn bst(&mut self, operand: usize) {
self.registers[1] = self.combo(operand) & 7;
}
fn jnz(&mut self, operand: usize) {
self.instruction_ptr = if self.registers[0] == 0 {
self.instruction_ptr + 2
} else {
operand
};
}
fn bxc(&mut self) {
self.registers[1] ^= self.registers[2];
}
fn out(&mut self, operand: usize) {
self.output.push((self.combo(operand) & 7) as u8);
}
fn bdv(&mut self, operand: usize) {
self.registers[1] = self.registers[0] >> self.combo(operand);
}
fn cdv(&mut self, operand: usize) {
self.registers[2] = self.registers[0] >> self.combo(operand);
}
fn run(&mut self) {
while self.instruction_ptr < self.opcode.len()-1 {
let instruction = self.opcode[self.instruction_ptr];
let operand = self.opcode[self.instruction_ptr+1] as usize;
match instruction {
0 => self.adv(operand),
1 => self.bxl(operand),
2 => self.bst(operand),
3 => self.jnz(operand),
4 => self.bxc(),
5 => self.out(operand),
6 => self.bdv(operand),
7 => self.cdv(operand),
_ => unreachable!()
}
if instruction != 3 {
self.instruction_ptr += 2;
}
}
}
}
pub fn run(input: &str) -> Result<(String, usize), ParseError> {
let computer = Computer::try_from(input)?;
let mut computer_1 = computer.clone();
computer_1.run();
let first = computer_1.output.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(",");
// For part 2, we take advantage of the fact that both inputs are
// structured in a way that makes every nth last output only dependent
// on the first n octal digits of A, excluding leading zeros.
let mut possible_starts = Vec::from([0]);
let mut next_possible_starts: Vec<usize>;
for idx in (0..computer.opcode.len()).rev() {
next_possible_starts = possible_starts
.iter()
.flat_map(|&start| (start << 3 .. (start + 1) << 3).filter(|&a| {
let mut computer = computer.clone();
computer.registers[0] = a;
computer.run();
computer.output == computer.opcode[idx..]
})).collect();
std::mem::swap(&mut possible_starts, &mut next_possible_starts);
}
let second = *possible_starts.iter().min().unwrap();
Ok((first, second))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::read_to_string;
fn read_file(name: &str) -> String {
read_to_string(name).expect(&format!("Unable to read file: {name}")[..]).trim().to_string()
}
#[test]
fn test_sample() {
let sample_input = read_file("tests/sample_input");
assert_eq!(run(&sample_input), Ok(("5,7,3,0".to_string(), 117440)));
}
#[test]
fn test_challenge() {
let challenge_input = read_file("tests/challenge_input");
assert_eq!(run(&challenge_input), Ok(("7,4,2,5,1,4,6,0,4".to_string(), 164278764924605)));
}
}

View file

@ -0,0 +1,5 @@
Register A: 17323786
Register B: 0
Register C: 0
Program: 2,4,1,1,7,5,1,5,4,1,5,5,0,3,3,0

View file

@ -0,0 +1,5 @@
Register A: 2024
Register B: 0
Register C: 0
Program: 0,3,5,4,3,0