After a mentally painful Day 16, I was very happy we got the first computer puzzle of the year. This is another Advent of Code stable where the first part is about implementing the rules and operations and then the second part is figuring out how to do things without actually running that computer.
Read input
Our input today comes in two parts: the registers and the program code.
Today I decided to skip regex since the input is simple enough.
To read the registers, we split the section to lines and manually point the values to correct registers. If the amount of registers was higher or they could be in any random order, I would have done it with regex.
The program is even simpler as we just split it from commas and convert to integers:
Part 1
The program input is a series of opcode
s and operand
s. There are eight opcodes in total:
0
is adv
where we divide the number from register A
with 2
to the power of a combo value of operand. combo
means if the operand
is 3 or smaller, it’s that number and if it’s 4-6, it gets the value from registers A
-C
. Final result is truncated to integer.
1
is bxl
which calculates bitwise XOR of the value in register B
and the literal value of operand:
2
is bst
which calculates the combo value modulo 8 and stores it into register B
:
3
is jnz
which is notorious for creating complexity in Advent of Code puzzles as it checks the value from register A
and if it is not zero, jumps to a new pointer value:
4
is bxc
which takes the XOR
of B
and C
and stores it back to B
:
5
is out
which outputs the value of the combo operand modulo 8:
6
is bdv
which is the same as adv
but the value is stored in register B
:
and 7
is cdv
which is the same as previous but stored in C
:
Our interface choice here is that we input registers
and operand
to each function (even bxc
which doesn’t do anything with it) and we output a tuple of Registers
, NewPointer
and Output
.
This unified interface makes it possible for us to store the functions in a dictionary and pick the right one by the opcode. Because of this, we don’t have to do any if
or match
logic inside our program processor:
To run this program, I call process for the program and join the output with commas:
Part 2
The second part throws in a new twist. What the program should be doing is returning its own input as output and our job is to find the smallest value of register A
that results in the same output as the input.
Usually these are about recognising patterns or loops that can be minified to avoid running every step.
So my first action was to reduce my input program into something simplified. My program, when written out, looked like the following.
Syntax legend
|name of the operation|(actual calculation)|→ where to store|
Math operations:
%
== modulo^
== XOR//
== integer division**
= exponential
First I noticed that jnz
only ever happens at the end and always jumps to the start. That’s a big relief because I’ve been working for days on Day 12 of 2016 where it’s more complex. If you got a kick out of today’s puzzle, that’s a nice one to solve next.
Then I noticed that A
only ever changes by being divided by 8.
So we can simplify the output calculation to:
A single expression in where we calculate the individual output in terms of A
without having to run the program.
I then put this into a function:
and tested that it still worked with the first step. Great, I got an optimised version of my original solution.
Even with that, running this brute force won’t work so the next step was to start thinking about the program output from the end towards the front.
To reach the end, the result of the final division (before integer truncation) needs to be inside the [0..1)
range ([
means included, )
means not included). So any number between 0 and 1 but not 1. The problem with that is that there’s an infinite amount of them and I don’t even know if we’d be looking for a larger or smaller end of that.
With a few hints, I was able to figure out that to get the last output, I’d need to run it with an A
value of less than 8. I could then find that number by looping through all the options and seeing if it matches:
which gave me the correct number.
Next, I would multiply that by 8 (as we divide by 8 going backwards) and try again, finding the next number that provides the correct sub-output (two last items):
I would then repeat this for as many times as there are numbers in the program:
The final A//8
is because we multiply it once too many.
This worked for the first (or last, depending on how you count) 10 outputs but then the rest were filled with 4
s. I figured it might have been because the range of potential a
s didn’t go far enough so I added a few.
This resulted in the following 2 star performance:
I used 8**i
because it felt appropriate. Since we divide/multiply by 8 every time (depending on the direction), the upper bound likely also multiplies with 8. As long as the upper limit is high enough, I guess it’s enough because we stop on the smallest one we find anyway.
Today was probably the kindest version of the “build a computer” puzzles over the years and I think it’s the first time I’ve gotten the second star.
I always get excited because implementing a system based on rules is fun. Finding patterns and using them to solve the second part is usually less to my liking but today was quite fun.
It feels good to be back on the 2-star gang.