Skip to main content

aoc/year2021/
day24.rs

1//! # Arithmetic Logic Unit
2//!
3//! There are two ways to solve this problem. We can brute force emulate the ALU, but even with
4//! memoization this takes a long time to solve. Instead analyzing and reverse engineering the code
5//! shows an insight that reduces the problem to a much simpler constraint satisfaction problem.
6//!
7//! ## Analysis Summary
8//!
9//! The code consists of 14 blocks of 18 instructions, each block starting with `inp w`.
10//!
11//! There are 7 "push" blocks and 7 "pop" blocks.
12//! * Push blocks use `z` as a stack, multiplying by 26 and adding `w` plus some constant `k₁`.
13//!   Push blocks always add to the stack.
14//! * Pop blocks compare the top element of `z` plus some constant `k₂` to `w`. If the values are
15//!   equal then `z` is "popped" by dividing by 26. Otherwise `z` is pushed again with `w`
16//!   plus some constant.
17//!
18//! Since the push/pop blocks are equally numbered and we want `z` to be zero (empty stack) at the
19//! end of the program, the condition:
20//!
21//! `w₁ + k₁ + k₂ = w₂`
22//!
23//! must be true for each matching pair of push/pop blocks. Since we also know that each `w` is
24//! between 1 and 9 inclusive, that is enough information to determine maximum and minimum values
25//! for each of the fourteen `w` values.
26//!
27//! For example:
28//! * If `k₁ + k₂ = 7` then `w₁ + 7 = w₂`.
29//! * Maximum value of `w₁` is 2 when `w₂` is 9.
30//! * Minimum value of `w₁` is 1 when `w₂` is 8.
31//!
32//! ## Detailed Push block analysis
33//!
34//! ```none
35//!     inp w       // w = 1 to 9 inclusive
36//!     mul x 0     // x = 0
37//!     add x z     // x = z
38//!     mod x 26    // x = z % 26
39//!     div z 1     // nop
40//!     add x 13    // x = z % 26 + 13
41//!     eql x w     // if (z % 26 + 13) == w { x = 1 } else { x = 0 }
42//!                 // However since w is restricted to 1 to 9 this condition is always false
43//!                 // so x is always 0. Other blocks have different constants but always > 9.
44//!     eql x 0     // x = 1 (as 0 == 0)
45//!     mul y 0     // y = 0
46//!     add y 25    // y = 25
47//!     mul y x     // y = 25 * 1 = 25
48//!     add y 1     // y = 25 + 1 = 26
49//!     mul z y     // z = 26 * z
50//!     mul y 0     // y = 0
51//!     add y w     // y = w
52//!     add y 14    // y = w + 14 (k₁ = 14)
53//!     mul y x     // y = (w + 14) * 1 = (w + 14)
54//!     add z y     // z = (26 * z) + (w + 14)
55//! ```
56//!
57//! ## Detailed Pop block analysis
58//!
59//! ```none
60//!     inp w       // w = 1 to 9 inclusive
61//!     mul x 0     // x = 0
62//!     add x z     // x = z
63//!     mod x 26    // x = z % 26
64//!     div z 26    // z /= 26 (pop)
65//!     add x -13   // x = z % 26 - 13 (k₂ = -13)
66//!     eql x w     // if (z % 26 - 13) == w { x = 1 } else { x = 0 }
67//!     eql x 0     // if (z % 26 - 13) == w { x = 0 } else { x = 1 }
68//!                 // Inverts the previous conditional.
69//!                 // Unlike the push blocks, this may be true or false
70//!                 // We'll split into 2 paths, depending on equals (x = 0) or
71//!                 // not equal (x = 1).
72//!                 | Equals (x = 0)        | Not Equals (x = 1)        |
73//!     mul y 0     | y = 0                 | y = 0                     |
74//!     add y 25    | y = 25                | y = 25                    |
75//!     mul y x     | y = 25 * 0 = 0        | y = 25 * 1 = 25           |
76//!     add y 1     | y = 0 + 1 = 1         | y = 25 + 1 = 26           |
77//!     mul z y     | z = z (nop)           | z = 26 * z                |
78//!     mul y 0     | y = 0                 | y = 0                     |
79//!     add y w     | y = w                 | y = w                     |
80//!     add y 4     | y = w + 4             | y = w + 4                 |
81//!     mul y x     | y = (w + 4) * 0       | y = (w + 4) * 1           |
82//!     add z y     | z = z                 | z = (26 * z) + (w + 4)    |
83//! ```
84use crate::util::parse::*;
85
86/// Convert matching pairs of blocks into constraints.
87/// For the first digit `value` is `-(k₁ + k₂)` and second digit value is `k₁ + k₂`.
88pub struct Constraint {
89    index: usize,
90    value: i32,
91}
92
93/// Convert `k₁ + k₂` to min and max values, clamping at 1 and 9 respectively.
94impl Constraint {
95    fn min(&self) -> i32 {
96        (1 + self.value).max(1)
97    }
98
99    fn max(&self) -> i32 {
100        (9 + self.value).min(9)
101    }
102}
103
104pub fn parse(input: &str) -> Vec<Constraint> {
105    let lines: Vec<_> = input.lines().collect();
106    let mut stack = Vec::new();
107    let mut constraints = Vec::new();
108
109    for (index, block) in lines.chunks(18).enumerate() {
110        // Register names contain no digits, so each line holds at most a single number.
111        let helper = |i: usize| block[i].signed::<i32>();
112
113        // The 5th instruction in "push" blocks is always a `div z 1`
114        // that we can use to figure out what type of block we're dealing with.
115        if helper(4) == 1 {
116            // `k₁` is always located at the 16th instruction.
117            stack.push(Constraint { index, value: helper(15) });
118        } else {
119            // Find the matching "push" instruction at the top of the stack.
120            let first = stack.pop().unwrap();
121            // `k₂` is always located at the 6th instruction, so delta = k₁ + k₂.
122            let delta = first.value + helper(5);
123            // w₁ + delta = w₂ <=> w₁ = w₂ - delta
124            constraints.push(Constraint { index: first.index, value: -delta });
125            constraints.push(Constraint { index, value: delta });
126        }
127    }
128
129    // Sort by original ALU program order.
130    constraints.sort_unstable_by_key(|c| c.index);
131    constraints
132}
133
134pub fn part1(input: &[Constraint]) -> String {
135    input.iter().map(|c| c.max().to_string()).collect()
136}
137
138pub fn part2(input: &[Constraint]) -> String {
139    input.iter().map(|c| c.min().to_string()).collect()
140}