Day 17: Chronospatial Computer
Megathread guidelines
- Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
- You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
Rust
First part was straightforward (the divisions are actually just right shifts), second part not so much. I made some assumptions about the input program, namely that in the end register 8 is divided by 8, then an output is made, then everything starts from the beginning again (if a isn’t 0). I found that the output always depends on at most 10 bits of a, so I ran through all 10-bit numbers and grouped them by the first generated output. At that point it’s just a matter of chaining these 10-bit numbers from the correct groups so that they overlap on 7 bits. The other 3 bits are consumed each round.
Solution
use rustc_hash::FxHashMap; fn parse(input: &str) -> Option<Program> { let mut lines = input.lines(); let a = lines.next()?.split_once(": ")?.1.parse().ok()?; let b = lines.next()?.split_once(": ")?.1.parse().ok()?; let c = lines.next()?.split_once(": ")?.1.parse().ok()?; lines.next()?; let program = lines .next()? .split_once(": ")? .1 .split(',') .map(|s| s.parse()) .collect::<Result<Vec<u8>, _>>() .ok()?; Some(Program { a, b, c, out: vec![], program, ip: 0, }) } #[derive(Debug, Clone, Default)] struct Program { a: u64, b: u64, c: u64, out: Vec<u8>, program: Vec<u8>, ip: usize, } impl Program { fn run(&mut self) { while self.step() {} } // Returns true if a step was taken, false if it halted fn step(&mut self) -> bool { let Some(&[opcode, operand]) = &self.program.get(self.ip..self.ip + 2) else { return false; }; self.ip += 2; match opcode { 0 => self.adv(self.combo(operand)), 1 => self.bxl(operand), 2 => self.bst(self.combo(operand)), 3 => self.jnz(operand), 4 => self.bxc(), 5 => self.out(self.combo(operand)), 6 => self.bdv(self.combo(operand)), 7 => self.cdv(self.combo(operand)), _ => panic!(), } true } fn combo(&self, operand: u8) -> u64 { match operand { 0..=3 => operand as u64, 4 => self.a, 5 => self.b, 6 => self.c, _ => unreachable!(), } } fn adv(&mut self, x: u64) { self.a >>= x } fn bxl(&mut self, x: u8) { self.b ^= x as u64 } fn bst(&mut self, x: u64) { self.b = x % 8 } fn jnz(&mut self, x: u8) { if self.a != 0 { self.ip = x as usize } } fn bxc(&mut self) { self.b ^= self.c } fn out(&mut self, x: u64) { self.out.push((x % 8) as u8) } fn bdv(&mut self, x: u64) { self.b = self.a >> x } fn cdv(&mut self, x: u64) { self.c = self.a >> x } } fn part1(input: String) { let mut program = parse(&input).unwrap(); program.run(); if let Some(e) = program.out.first() { print!("{e}") } for e in program.out.iter().skip(1) { print!(",{e}") } println!() } // Some assumptions on the input: // * There is exactly one jump instruction at the end of the program, jumping to 0 // * Right before that, an output is generated // * Right before that, register a is shifted right by 3: adv(3) // // Each output depends on at most 10 bits of a (it is used with a shift of at most 7). // Therefore we look at all 10-bit a's and group them by the first number that is output. // Then we just need to combine these generators into a chain that fits together. fn number_generators(mut program: Program) -> [Vec<u16>; 8] { let mut out = [const { vec![] }; 8]; for a in 1..(1 << 10) { program.a = a as u64; program.out.clear(); program.ip = 0; program.run(); let &output = program.out.first().unwrap(); out[output as usize].push(a); } out } fn part2(input: String) { let mut program = parse(&input).unwrap(); let generators = number_generators(program.clone()); let output = program.program.clone(); // a_candidates maps from 7-bit required prefixes to the lower bits of a that // generate the required numbers so far. let mut a_candidates: FxHashMap<u8, u64> = generators[output[0] as usize] .iter() .rev() // Collects the values for each prefix .map(|&a| ((a >> 3) as u8, a as u64 % 8)) .collect(); let len = output.len(); for (i, x) in output.iter().enumerate().skip(1) { let mut next_candidates = FxHashMap::default(); for (prefix, val) in generators[*x as usize] .iter() .filter(|&a| { // Take only short candidates in the end to ensure that not too many numbers are generated let max_bits = (len - i) * 3; (*a as u64) < (1u64 << max_bits) }) // Only use generators that match any required prefix .filter(|&a| a_candidates.contains_key(&((a % (1 << 7)) as u8))) .map(|&a| { let prefix = (a >> 3) as u8; let val = a as u64 % 8; let prev = a_candidates[&((a % (1 << 7)) as u8)]; (prefix, (val << (i * 3)) | prev) }) { // Only insert first (smallest) encountered value next_candidates.entry(prefix).or_insert(val); } a_candidates = next_candidates; } println!("{}", a_candidates[&0]); // Verify result program.a = a_candidates[&0]; program.run(); assert_eq!(program.out, program.program); } util::aoc_main!();
Also on github
Your code helped me find my bug, thanks.
Wild that all the examples can be passed with an incorrect
cdv
instruction, I didnt read the last part properly and assumed it was C /= operand. :(