diff --git a/2024/day17_chronospatial_computer/Cargo.toml b/2024/day17_chronospatial_computer/Cargo.toml new file mode 100644 index 0000000..16d2099 --- /dev/null +++ b/2024/day17_chronospatial_computer/Cargo.toml @@ -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 diff --git a/2024/day17_chronospatial_computer/challenge.md b/2024/day17_chronospatial_computer/challenge.md new file mode 100644 index 0000000..c4c9d3e --- /dev/null +++ b/2024/day17_chronospatial_computer/challenge.md @@ -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). \ No newline at end of file diff --git a/2024/day17_chronospatial_computer/src/lib.rs b/2024/day17_chronospatial_computer/src/lib.rs new file mode 100644 index 0000000..4477f5a --- /dev/null +++ b/2024/day17_chronospatial_computer/src/lib.rs @@ -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 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, + instruction_ptr: usize, + registers: [usize; 3], + output: Vec, +} + +impl TryFrom<&str> for Computer { + type Error = ParseError; + + fn try_from(value: &str) -> Result { + 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::()?; + let b = lines[1].split_whitespace().last().ok_or(Self::Error::RegisterNotSet(2))?.parse::()?; + let c = lines[2].split_whitespace().last().ok_or(Self::Error::RegisterNotSet(3))?.parse::()?; + + let registers = [a, b, c]; + + let opcode = lines[4] + .split_whitespace() + .last() + .ok_or(Self::Error::ProgramNotSet)? + .split(',') + .map(|n| n.parse::()) + .collect::, _>>()?; + + 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::>().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; + 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))); + } +} diff --git a/2024/day17_chronospatial_computer/tests/challenge_input b/2024/day17_chronospatial_computer/tests/challenge_input new file mode 100644 index 0000000..a12959d --- /dev/null +++ b/2024/day17_chronospatial_computer/tests/challenge_input @@ -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 diff --git a/2024/day17_chronospatial_computer/tests/sample_input b/2024/day17_chronospatial_computer/tests/sample_input new file mode 100644 index 0000000..4a91c26 --- /dev/null +++ b/2024/day17_chronospatial_computer/tests/sample_input @@ -0,0 +1,5 @@ +Register A: 2024 +Register B: 0 +Register C: 0 + +Program: 0,3,5,4,3,0