# AoC 2021 Day 24: Aluinator

### Source: Arithmetic Logic Unit

#### Part 1: Simulate an ALU with 4 registers (w, x, y, and z) and instructions defined below. Find the largest 14 digit number with no 0 digits which result in z=0.

Instructions:

• inp a - read the next input value into register a
• add a b - set a = a + b (b can always be a register or an integer)
• mul a b - set a = a * b
• div a b - set a = a / b (using integer division / discarding the remainder)
• mod a b - set a = a % b (% is the modulo operator)
• eql a b - set a = 1 if a == b, set a = 0 otherwise

Fascinating. So there are a number of different ways you can approach this problem.

First, we can just implement the ALU and try to brute force it:

OPS = {
'add': lambda a, b: a + b,
'mul': lambda a, b: a * b,
'div': lambda a, b: a // b,
'mod': lambda a, b: a % b,
'eql': lambda a, b: 1 if a == b else 0,
'neq': lambda a, b: 1 if a != b else 0,
}

@app.command()
def run(file: typer.FileText, input: str, quiet: bool = False):
input_digits = [int(c) for c in input]

registers = {
'w': 0,
'x': 0,
'y': 0,
'z': 0,
}

for i, line in enumerate(file, 1):
line = line.strip()
logging.debug(f'[{i:04d} {registers=}] {line}')

cmd, *args = line.split()

if cmd == 'inp':
a = args
registers[a] = input_digits
input_digits = input_digits[1:]

else:
a = args
b = args

a_val = registers[a]
b_val = registers[b] if b in registers else int(b)

r_val = OPS[cmd](a_val, b_val)

registers[a] = r_val

if quiet:
return registers['z']
else:
print(registers['z'])

@app.command()
def part1(file: typer.FileText):

for i in range(99999999999999, 1, -1):
input = str(i)
if '0' in input:
continue

file.seek(0)

result = run(file, input, True)
if result == 0:
break

if input.endswith('9999'):
logging.info(f'{input} -> {result}')

print(input)


Very short/clean code. Will take… forever to run. It can crank through roughly 10,000 values in 2-3 seconds. Given that we have 1 quadrillion to check (ignoring the 0 condition for now), that’s… 6000 years to check them all. Even assuming the first two digits are 9, that’s still 63 years.

Yeah, no.

Disclaimer, this was not actually my first solution to the problem. I actually wrote it up so that I could verify solutions obtained other ways, so let’s back up and try a few other approaches.

##### Version 1: State with input paths

For my first real attempt, I didn’t look at the input code (which would really have helped). Instead, I jumped straight in to a state map sort of like this:


state = {
'w': {0: {}},
'x': {0: {}},
'y': {0: {}},
'z': {0: {}},
}

input_counter = 0
for i, (cmd, a, b) in enumerate(code, 1):
logging.info(f'[{i:04d}] {cmd} {a} {b}')

# Take the current input value and add that to the state to get here
if cmd == 'inp':
state[a] = {
digit: {input_counter: {digit}}
for digit in ALL_INPUTS
}
input_counter += 1

# Otherwise, try to apply the given operation
# But only on input states that match
else:
op = OPS[cmd]

# If we have a constant, just apply it (path hasn't changed)
if isinstance(b, int):
state[a] = {
op(key, b): key_path
for key, key_path in state[a].items()
}

# Otherwise, apply all changes if the paths are the same
else:
new_output = defaultdict(lambda: defaultdict(set))

for key, key_path in state[a].items():
logging.debug(f'{PRE}- {key=}, {key_path=}')

for value, value_path in state[b].items():
result = op(key, value)
logging.debug(f'{PRE}-- {value=}, {value_path=}, {result=}')

all_indexes = key_path.keys() | value_path.keys()

# There are no restrictions on inputs at all
if not key_path and not value_path:
logging.debug(f'{PRE}--- No restrictions')
if result not in new_output:
new_output[result] = defaultdict(set)

# Key path is known but not value path
if not value_path:
logging.debug(f'{PRE}--- No restriction on value')
for i, inputs in key_path.items():
new_output[result][i] |= inputs

# Value path is known, but not key
elif not key_path:
logging.debug(f'{PRE}--- No restriction on key')
for i, inputs in value_path.items():
new_output[result][i] |= inputs

# There are no overlapping indexes, include both
#elif not any(
#    index in key_path and index in value_path
#    for index in all_indexes
#):
#    logging.debug(f'{PRE}--- No overlapping indexes, combine')
#    for index in all_indexes:
#        new_output[result][index] = key_path.get(index) or value_path.get(index)

# The indexes are compatible
elif any(
index not in key_path or index not in value_path or key_path[index] == value_path[index]
for index in all_indexes
):
logging.debug(f'{PRE}--- Compatible indexes, combine')
for index in all_indexes:
new_output[result][index] = key_path.get(index) or value_path.get(index)

else:
raise NotImplementedError

new_output = {
k1: {
k2: s
for k2, s in new_output[k1].items()
if s != ALL_INPUTS
}
for k1 in new_output
}
logging.debug(f'{PRE}{new_output=}')
state[a] = new_output

#logging.info(f'\n{pformat(state)}')

# DEBUG
#if i > 25:
#    break

print()
#pprint(state)

pprint(state['z'])


It’s… quite a mess. The basic idea is we have this nutty data structure where each state (of the 4 registers) stores all of the ways we could get to it. That’s a dictionary of {register: {input_key: set(input_value)}}. This ignores the fact that w is the only register written to, it’s always written to in input order (which we do know from the problem statement), and was a royal pain to write. So… let’s try again.

##### Version 2: Parsing into an Abstract Syntax Tree

For attempt 2, I’m still going to try to solve for ‘all values’, but this time I’m going to do it by parsing out the eventual abstract syntax tree for all values. As another way of saying it, if we have this program:

inp w
mul x 0
mod x 26


You will get this sequence of trees:

# inp w
I(0)

# mul x 0
mul
├ I(0)
└ 0

├ mul
│ ├ I(0)
│ └ 0
└ R(z) # whatever Z previously was

# mod x 26
mod x 26
│ ├ mul
│ │ ├ I(0)
│ │ └ 0
│ └ R(z) # whatever Z previously was
└ 26


Pretty neat. And the code’s not bad either:

@dataclass(frozen=True)
class Node:
pass

@dataclass(frozen=True)
class Literal(Node):
value: int

def __call__(self, input: str, depth: int = 0) -> int:
return self.value

@dataclass(frozen=True)
class Input(Node):
index: int

def __call__(self, input: str, depth: int = 0) -> int:
return int(input[self.index])

@dataclass(frozen=True)
class Operator(Node):
function: str
left: Node
right: Node

def __call__(self, input: str, depth: int = 0) -> int:
l_value = self.left(input, depth + 1)
r_value = self.right(input, depth + 1)
return OPS[self.function](l_value, r_value)


And a parser:

def parse(file: TextIO) -> Node:
'''Read input into a list of instructions.'''

registers = {
'w': Literal(0),
'x': Literal(0),
'y': Literal(0),
'z': Literal(0),
}

next_input_index = 0

for i, line in enumerate(file, 1):
line = line.strip()
logging.info(f'[{i:04d}] Parsing {line}')

cmd, *args = line.strip().split()
a = args

if cmd == 'inp':
registers[a] = Input(next_input_index)
next_input_index += 1
continue

b = args
if not b.isalpha():
right = Literal(int(b))
else:
right = registers[b]

registers[a] = Operator(cmd, registers[a], right)

return registers['z']


That will return a callable/function that you can call on every value to get what the result of z is. Problem is… these trees are gigantic. So I played a neat optimization trick where I can do partial evaluation:

@dataclass(frozen=True)
class Node:
pass

def partial_eval(self) -> 'Node':
return self

@dataclass(frozen=True)
class Operator(Node):
function: str
left: Node
right: Node

@disableable_cache
def partial_eval(self) -> 'Node':
l_value = self.left.partial_eval()
r_value = self.right.partial_eval()

# Literal evals can be directly simplified
if isinstance(l_value, Literal) and isinstance(r_value, Literal):
try:
result = Literal(OPS[self.function](l_value.value, r_value.value))
except ZeroDivisionError:
result = Literal(0)

logging.info(f'{PRE} Simplifying {self} -> {result}')
return result

elif self.function == 'add' and isinstance(l_value, Literal) and l_value.value == 0:
logging.info(f'{PRE} Applying 0+a=a')
return r_value

elif self.function == 'add' and isinstance(r_value, Literal) and r_value.value == 0:
logging.info(f'{PRE} Applying a+0=a')
return l_value

# multiplicative identity
elif self.function == 'mul' and isinstance(l_value, Literal) and l_value.value == 1:
logging.info(f'{PRE} Applying 1*a=a')
return r_value

elif self.function == 'mul' and isinstance(r_value, Literal) and r_value.value == 1:
logging.info(f'{PRE} Applying a*1=a')
return l_value

elif self.function == 'mul' and isinstance(l_value, Literal) and l_value.value == 0:
logging.info(f'{PRE} Applying 0*a=a')
return Literal(0)

elif self.function == 'mul' and isinstance(r_value, Literal) and r_value.value == 0:
logging.info(f'{PRE} Applying a*0=a')
return Literal(0)

# division identities
elif self.function == 'div' and isinstance(r_value, Literal) and r_value.value == 1:
logging.info(f'{PRE} Applying a/1=a')
return l_value

elif self.function == 'div' and l_value == r_value:
logging.info(f'{PRE} Applying a/a=1')
return Literal(1)

# equality identities
elif self.function == 'eql' and l_value == r_value:
logging.info(f'{PRE} Applying a == a -> 1')
return Literal(1)

elif (
self.function == 'eql'
and isinstance(l_value, Operator)
and l_value.function == 'eql'
and isinstance(r_value, Literal)
and r_value.value == 0
):
result = Operator('neq', l_value.left, l_value.right)
logging.info(f'{PRE} Converting eq to neq')
return result

# If we're doing EQL any input to a value not [0, 9], that's always 0
elif (
self.function == 'eql'
and isinstance(l_value, Literal)
and (l_value.value < 0 or l_value.value > 9)
and isinstance(r_value, Input)
):
logging.info(f'{PRE} Simplifying impossible {self} -> 0')
return Literal(0)

elif (
self.function == 'eql'
and isinstance(r_value, Literal)
and (r_value.value < 0 or r_value.value > 9)
and isinstance(l_value, Input)
):
logging.info(f'{PRE} Simplifying impossible {self} -> 0')
return Literal(0)

elif (
self.function == 'neq'
and isinstance(l_value, Literal)
and (l_value.value < 0 or l_value.value > 9)
and isinstance(r_value, Input)
):
logging.info(f'{PRE} Simplifying impossible {self} -> 0')
return Literal(1)

elif (
self.function == 'neq'
and isinstance(r_value, Literal)
and (r_value.value < 0 or r_value.value > 9)
and isinstance(r_value, Input)
):
logging.info(f'{PRE} Simplifying impossible {self} -> 0')
return Literal(1)

# Base case
else:
return self


As noted, there are a lot of mathematical operations (like a*0=0) that we can apply to cut out huge swatches of the AST. Especially since these are actually used in our input to zero out variables, for example.

And that really does work, it makes the trees quite a bit smaller. But… it’s still gigantic and takes much longer than just directly evaluating the answers (in version 0). So… I abandoned this one as well, cool as it was.

##### Version 3: State tree redux + golang

Okay, version 3. This time I was going to more cleanly do the state map that I tried in version 1. This time, the map is {registers: input string}. Since I know I’m looking for the largest string, I only have to keep that.

@dataclass(frozen=True)
class State:
w: int
x: int
y: int
z: int

def __getitem__(self, key: Union[str, int]) -> int:
if key in 'wxyz':
return getattr(self, key)

return int(key)

def set(self, key: str, value: int) -> 'State':
return State(
value if key == 'w' else self.w,
value if key == 'x' else self.x,
value if key == 'y' else self.y,
value if key == 'z' else self.z,
)

def __repr__(self):
return f'{{{self.w}, {self.x}, {self.y}, {self.z}}}'


And the actual state map:

def read(file: TextIO):
start = time.perf_counter()
states = {State(0, 0, 0, 0): ''}

input_length = 0
for i, line in enumerate(file, 1):
line = line.strip()
logging.info(
f'[line={i:04d} len={input_length} states={len(states)} time={time.perf_counter() - start:02f}s] {line}')

cmd, *args = line.split()

if cmd == 'inp':
# Now expand again with the new inputs
a = args
new_states = {}
input_length += 1

for i in ALL_INPUTS:
for state, input in states.items():
new_state = state.set(a, i)
new_input = input + str(i)

if new_state not in new_states:
new_states[new_state] = new_input

new_states[new_state] = max(new_states[new_state], new_input)

states = new_states

else:
# Otherwise apply the function to all current states
a, b = args
states = {
state.set(a, OPS[cmd](state[a], state[b])): input
for state, input in states.items()
}

result = None

logging.info('Scanning for final solution')
for state, input in states.items():
if state.z != 0:"""  """
continue

if not result or input > result:
logging.info(f'New maximum found: {input}')
result = input

return result


Let’s see it with debug mode on:

$python3 aluinator-v3.py --debug part1 input.txt # It runs pretty fast at first! 11:24:21 INFO [line=0001 len=0 states=1 time=0.000013s] inp w 11:24:21 INFO [line=0002 len=1 states=9 time=0.000126s] mul x 0 11:24:21 INFO [line=0003 len=1 states=9 time=0.000165s] add x z 11:24:21 INFO [line=0004 len=1 states=9 time=0.000194s] mod x 26 11:24:21 INFO [line=0005 len=1 states=9 time=0.000222s] div z 1 11:24:21 INFO [line=0006 len=1 states=9 time=0.000250s] add x 10 ... # But every time there's an inp, we multiple states by 9... 11:24:22 INFO [line=0090 len=5 states=7290 time=0.268895s] add z y 11:24:22 INFO [line=0091 len=5 states=7290 time=0.276940s] inp w 11:24:22 INFO [line=0092 len=6 states=65610 time=0.392064s] mul x 0 ... # We do sometimes simplify a bunch of states (with things like mul y 0, which is basically y = 0) 11:24:22 INFO [line=0098 len=6 states=65610 time=0.945848s] eql x 0 11:24:22 INFO [line=0099 len=6 states=65610 time=1.051166s] mul y 0 11:24:22 INFO [line=0100 len=6 states=11340 time=1.136822s] add y 25 11:24:22 INFO [line=0101 len=6 states=11340 time=1.149788s] mul y x ... # And even by the 8th input, we're already taking about a second per command # And we're only around halfway through the program and have 6 more 9x of state counts... 11:24:29 INFO [line=0131 len=8 states=649539 time=7.659318s] div z 26 11:24:30 INFO [line=0132 len=8 states=649539 time=8.575231s] add x -12 ... # Yeah, 20 seconds per iteration isn't great (we're just about to inp again) 11:31:08 INFO [line=0179 len=10 states=5970510 time=406.829251s] mul y x 11:31:29 INFO [line=0180 len=10 states=5970510 time=427.306535s] add z y ...  And what’s worse, by the time I got to here, I was eating up 32GB of RAM and getting into swap space. So… that didn’t help runtime. In any case, I didn’t let this run out. I did try to rewrite it in golang though! package main import ( "bufio" "fmt" "log" "os" "strconv" "strings" ) type state struct { w int x int y int z int } func (s state) get(r string) int { if r == "w" { return s.w } else if r == "x" { return s.w } else if r == "y" { return s.y } else if r == "z" { return s.z } else { result, err := strconv.Atoi(r) if err != nil { log.Panic(err) } return result } } func (s state) set(r string, v int) state { result := s if r == "w" { result.w = v } else if r == "x" { result.x = v } else if r == "y" { result.y = v } else if r == "z" { result.z = v } return result } type input struct { min string max string } func minput(i1 input, i2 input) input { result := input{"", ""} if i1.min < i2.min { result.min = i1.min } else { result.min = i2.min } if i1.max > i2.max { result.max = i1.max } else { result.max = i2.max } return result } type Op func(int, int) int func is_reg(s string) bool { return s == "w" || s == "x" || s == "y" || s == "z" } func main() { // Store a list of operators operators := make(map[string]Op) operators["add"] = func(a int, b int) int { return a + b } operators["mul"] = func(a int, b int) int { return a * b } operators["div"] = func(a int, b int) int { if b == 0 { return 0 } else { return a / b } } operators["mod"] = func(a int, b int) int { if b == 0 { return 0 } else { return a % b } } operators["eql"] = func(a int, b int) int { if a == b { return 1 } else { return 0 } } // Open the input file file, err := os.Open("input.txt") if err != nil { log.Fatal(err) } defer file.Close() // Initialize the register states var states map[state]input var new_states map[state]input states = make(map[state]input) states[state{0, 0, 0, 0}] = input{"", ""} // Start scanning through the file line by line scanner := bufio.NewScanner(file) input_length := 0 line_number := 0 for scanner.Scan() { line_number += 1 line := scanner.Text() fields := strings.Fields(line) log.Printf("[%d len=%d states=%d] %s", line_number, input_length, len(states), line) // Build a new set of states from the current set new_states = make(map[state]input) // An input value adds 9 new states for each old state, each possible non-zero input if fields == "inp" { a := fields for i := 1; i <= 9; i++ { for old_state, old_input := range states { // Update the proper variable in the input string new_state := old_state.set(a, i) // Calculate the new minimum/maximum input string new_input := input{fmt.Sprint(old_input.min, i), fmt.Sprint(old_input.max, i)} // Check if we've already stored that in our new_state, if so keep the min/max if prev_new_input, ok := new_states[new_state]; ok { new_states[new_state] = minput(prev_new_input, new_input) } else { new_states[new_state] = new_input } } } } else { // Fetch the proper operator function f, ok := operators[fields] if !ok { log.Fatal("Unknown operator", fields) } a := fields // Update all old states for old_state, old_input := range states { b := fields a_val := old_state.get(a) b_val := old_state.get(b) r_val := f(a_val, b_val) new_state := old_state.set(a, r_val) // Check if we've already stored that in our new_state, if so keep the min/max if prev_new_input, ok := new_states[new_state]; ok { new_states[new_state] = minput(prev_new_input, old_input) } else { new_states[new_state] = old_input } } } // Finally, swap new_state to state states = new_states } result := input{"", ""} result_set := false for state, input := range states { if state.z != 0 { continue } if !result_set { result = input result_set = true continue } result = minput(result, input) } fmt.Println(result) }  That’s not actually that bad! I should write more go. And while it did work faster at first… it had a lot more issues with RAM (might be how I wrote it and a lack of successful GC?). When I came back in the morning, my computer had actually rebooted, so… that wasn’t great. Fun to write though? ##### Version 4: Let’s just do it by hand So… let’s try something completely different. One you I noticed (finally) was that the input was 14 almost identical blocks that looked like this: inp w mul x 0 add x z mod x 26 div z 1 # zdiv, this can be 1 or 26 add x 10 # cx eql x w eql x 0 mul y 0 add y 25 mul y x add y 1 mul z y mul y 0 add y w add y 10 # cy mul y x add z y  There are 3 constants that change, which I’ve named zdiv, cx, and cy. zdiv cx cy 1 10 10 1 13 5 1 15 12 26 -12 12 1 14 6 26 -2 6 1 13 15 26 -12 15 1 15 7 1 11 11 26 -3 11 26 -13 7 26 -12 5 26 -13 10  All of the rest of the code is the same. We can actually de-compile it if we want: def f(z, i, zdiv, cx, cy): '''Take the previous z, the input value, and two constants.''' # mul x 0 # add x z # mod x 26 # div z 1 -- zdiv (either 1 or 26) # add x 10 -- cx # >> x = (z // zdiv) % 26 + cx # eql x w # eql x 0 # >> x is 1 if x != i x_flag = (i == (z // zdiv) % 26 + cx) # mul y 0 # add y 25 # mul y x # >> because x is 0 or 1 if i == z % 26 + cx # >> y == 0 or 25 if i == z % 26 + cx # add y 1 # mul z y # >> y == 1 or 26 if i == z % 26 + cx # so: # >> z *= 26 if i == z % 26 + cx # >> z *= 1 (nop) if i != z % 26 + cx if x_flag: z *= 26 # mul y 0 # add y w # add y 10 -- cy # mul y x # add z y if x_flag: z += i + cy  Which ends up with this very compact block: def f(z, i, zdiv, cx, cy): if (i == (z // zdiv) % 26 + cx): return z * 26 + i + cy  So… what’s interesting about that? Well, we’re always either multiplying, dividing, or modding z by 26. That makes me think of a base 26 number (or a letter)? And that last bit, zdiv… that’s either 1 (which dividing by 1 is a no-op) or 26, removing a character from the base26 number. Hmm… Okay. Let’s think of z as a stack of base-26 numbers (I know, crazy right?). Each successful iteration, we’re going to advance the stack (multiplying by 26) and push i + cy onto the stack. But if we have a z / 26 step, that means we’re essentially going to pop off the stack, checking the value against + cx… which makes an equality between two different input values! zd cx cy stack / equations 1 10 10 i1+10 1 13 5 i1+10, i2+5 1 15 12 i1+10, i2+5, i3+12 26 -12 12 i4 = i3 + 12 - 12 = i3 1 14 6 i1+10, i2+5, i5+6 26 -2 4 i6 = i5 + 6 - 2 = i5 + 4 1 13 15 i1+10, i2+5, i7+15 26 -12 3 i8 = i7 + 15 - 12 = i7 + 3 1 15 7 i1+10, i2+5, i9+7 1 11 11 i1+10, i2+5, i9+7, i10+11 26 -3 2 i11 = i10 + 11 - 3 = i10 + 8 26 -13 12 i12 = i9 + 7 - 13 = i9 - 6 26 -12 4 i13 = i2 + 5 - 12 = i2 - 7 26 -13 11 i14 = i1 + 10 - 13 = i1 - 3  So each time zd=1, add i#+cy to the stack, so we add i1+10, then i2+5, then i3+12. Then we get our first zd=26, so we’ll pop the i3+12 off the stack and combine it with i4 and the current cx: i4 = i3 + 12 - 12 i4 = i3  Interesting. We continue as above, eventually creating a system of 7 equations that relate all 14 variables. That should be enough to generate all possible output values! i4 = i3 i6 = i5 + 4 i8 = i7 + 3 i11 = i10 + 8 i12 = i9 - 6 i13 = i2 - 7 i14 = i1 - 3  But what’s more, we know we’re looking for the largest. So for each pair, we’ll always set (at least) one 9 (the larger value) and then use the question to set the smaller. So i4 = i3 = 9, but if i6 = 9, then 9 = i5 + 4 so i5 = 5. And so on: ---------11111 12345678901234 ..99.......... ..9959........ ..995969...... ..995969.19... ..9959699193.. .999596991932. 99995969919326  Trying that against our simulator/version 0: $ python3 aluinator.py run input.txt 99995969919326

0


BAM! And the first 4 digits were all 9, so it wouldn’t have taken 63 years… just 30 weeks. Yay 2022 I suppose?

That is very cool. It’s not nearly as ‘code’ as many of the other solutions, but it’s still pretty awesome IMO.

#### Part 2: Find the smallest such 14 digit number with no 0 digits such that z=0.

Nothing changes, we already have the equations! We just need to the earlier value to 1:

---------11111
12345678901234
..11..........
..1115........
..111514......
..111514.19...
..1115147191..
.811151471911.
48111514719111


Verify:

$python3 aluinator.py run input.txt 48111514719111 0  BAM! #### Generating solutions Okay, now that I see how to solve the problem, I want to actually write a bit more code. Let’s write something that can pull out those constants and do the same equations: @app.command() def solve(file: typer.FileText): blocks = [] zdiv, cx, cy = 0, 0, 0 logging.info('Finding block parameters') for i, line in enumerate(file, 1): line = line.strip() last = line.split()[-1] logging.debug(f'[{i:04d} {zdiv=} {cx=} {cy=}] {line}') if line.startswith('inp'): if zdiv: logging.info(f'Block found: {zdiv=}, {cx=}, {cy=}') blocks.append((zdiv, cx, cy)) elif line.startswith('div z ') and last not in 'wxyz': zdiv = int(line.split()[-1]) elif line.startswith('add x ') and last not in 'wxyz': cx = int(line.split()[-1]) elif line.startswith('add y ') and last not in 'wxyz': cy = int(line.split()[-1]) logging.info(f'Block found: {zdiv=}, {cx=}, {cy=}') blocks.append((zdiv, cx, cy)) logging.info('----- ----- -----') logging.info('Generating equations') stack = [] equations = [] for i, (zdiv, cx, cy) in enumerate(blocks): if zdiv == 1: stack.append((i, cy)) logging.info(f'{zdiv:<4d} {cx:<4d} {cy:<4d} {stack}') else: j, cy = stack.pop() equations.append((i, j, cx + cy)) logging.info(f'{zdiv:<4d} {cx:<4d} {cy:<4d} i{i} = i{j} + {cx} + {cy} = i{j} + {cx+cy}') logging.info('----- ----- -----') logging.info('Solving for minimum/maximum') part1 = ['?'] * 14 part2 = ['?'] * 14 for i, j, delta in equations: i, j = min(i, j), max(i, j) if delta <= 0: part1[i], part1[j] = '9', str(9 + delta) part2[i], part2[j] = str(1 + abs(delta)), '1' else: part1[i], part1[j] = str(9 - delta), '9' part2[i], part2[j] = '1', str(1 + delta) equation = f'i{i} = i{j} + {delta}' logging.info(f'{equation:<15s} {"".join(part1)} {"".join(part2)}') print('part1:', ''.join(part1)) print('part2:', ''.join(part2))  It’s the same idea, but with nice logging output: $ python3 aluinator.py --debug solve input.txt

Finding block parameters
Block found: zdiv=1, cx=10, cy=10
Block found: zdiv=1, cx=13, cy=5
Block found: zdiv=1, cx=15, cy=12
Block found: zdiv=26, cx=-12, cy=12
Block found: zdiv=1, cx=14, cy=6
Block found: zdiv=26, cx=-2, cy=4
Block found: zdiv=1, cx=13, cy=15
Block found: zdiv=26, cx=-12, cy=3
Block found: zdiv=1, cx=15, cy=7
Block found: zdiv=1, cx=11, cy=11
Block found: zdiv=26, cx=-3, cy=2
Block found: zdiv=26, cx=-13, cy=12
Block found: zdiv=26, cx=-12, cy=4
Block found: zdiv=26, cx=-13, cy=11
----- ----- -----
Generating equations
1    10   10    [(0, 10)]
1    13   5     [(0, 10), (1, 5)]
1    15   12    [(0, 10), (1, 5), (2, 12)]
26   -12  12    i3 = i2 + -12 + 12 = i2 + 0
1    14   6     [(0, 10), (1, 5), (4, 6)]
26   -2   6     i5 = i4 + -2 + 6 = i4 + 4
1    13   15    [(0, 10), (1, 5), (6, 15)]
26   -12  15    i7 = i6 + -12 + 15 = i6 + 3
1    15   7     [(0, 10), (1, 5), (8, 7)]
1    11   11    [(0, 10), (1, 5), (8, 7), (9, 11)]
26   -3   11    i10 = i9 + -3 + 11 = i9 + 8
26   -13  7     i11 = i8 + -13 + 7 = i8 + -6
26   -12  5     i12 = i1 + -12 + 5 = i1 + -7
26   -13  10    i13 = i0 + -13 + 10 = i0 + -3
----- ----- -----
Solving for minimum/maximum
i2 = i3 + 0     ??99?????????? ??11??????????
i4 = i5 + 4     ??9959???????? ??1115????????
i6 = i7 + 3     ??995969?????? ??111514??????
i9 = i10 + 8    ??995969?19??? ??111514?19???
i8 = i11 + -6   ??9959699193?? ??1115147191??
i1 = i12 + -7   ?999596991932? ?811151471911?
i0 = i13 + -3   99995969919326 48111514719111

part1: 99995969919326
part2: 48111514719111


Fast too! (As it should be):

--- Day 24: Arithmetic Logic Uni ---

\$ python3 aluinator.py solve input.txt
part1: 99995969919326
part2: 48111514719111
# time 50759375ns / 0.05s


A bit better than 30 weeks. :D

Anyways. That was weird and fun. A neat change of pace. Onward and lastward!