--- Day 24: ALU - Woo Hoo ---

December 24

Listening to: Christmas Lo-Fi


This one seems kinda fun - make a basic ALU. It's a bit more accurate to say it's implementing a very limited assembly language, with some ALU instructions and an input. Seems fun!

... Though finishing reading the problem, it seems there's a little bit of a hitch. Besides running the programm, I have to validate a model number, that's 14 digits, and I don't know what it is aside from it having no 0 digits. Great. So it's also kind of a searching problem. There's 14^9 = 20,661,046,784. That's a lot of possibilities. I need to find the largest, so I can work from 99999999999999 backwards. Still, in the worst case, the only valid number has all 1s.

There's also some other, unknown restriction as well. No idea what it is though, so I'll have to figure it out. I guess?

Anyway, cart before horse. Let's get that ALU running!

class ALU {
    private:
    vector<string> program;

    public:
    int w, x, y, z;

    ALU(){}
    ALU(vector<string> prog) { program = prog; w = 0; x = 0; y = 0; z = 0; }
    void Print() { printf("W = [%2.1d]\nX = [%2.1d]\nY = [%2.1d]\nZ = [%2.1d]\n\n", w, x, y, z); }

    void RunProgram(string input) {
        int a, b;
        map<char, int> s; // State
        s['w'] = 0; s['x'] = 0; s['y'] = 0; s['z'] = 0;
        for (auto i: program) {
            if (i[1] != 'n') { // Not Input 
                a = i[6] >= 'w' && i[6] <= 'z';
                if (!a) b = stoi(i.substr(6));
            }

            switch(i[1]) { // Second character is unique for each instruction
                case 'n': s[i[4]] = input[0] - '0'; input.erase(0,1); break; // iNput
                case 'd': s[i[4]] += a ? s[i[6]] : b; break; // aDd
                case 'u': s[i[4]] *= a ? s[i[6]] : b; break; // mUltiply
                case 'i': s[i[4]] /= a ? s[i[6]] : b; break; // dIvide
                case 'o': s[i[4]] %= a ? s[i[6]] : b; break; // mOdulo
                case 'q': s[i[4]] = s[i[4]] == (a ? s[i[6]] : b); break; // eQual
            }
        }
        w = s['w']; x = s['x']; y = s['y']; z = s['z'];
    }
};

~

Okay, the initial, naieve solution of bruteforcing it from the highest possible model number isn't going to work, I don't think. It takes about 30 seconds to go through 1,000,000 units. It would take maybe months to run this all the way through on average. I'm going to have to work out what's going on by hand.

NOTE: To anyone that wants to compare my analysis of the input to their own, keep in mind, everyone gets their own input file. They may have similarities, but while writing this, I don't know what those similarities are. Anything I say is only of my own input, not the inputs in general.

It looks repetitive, so splitting the programm up on the input lines confirms my first thought: It's (mostly) the same program running 14 times. The differences on mine are

* Lines 6 and 16 add a different number each iteration
* Line 5 variably divides by either 1 or 26.

Each iteration also ends on the instruction add z y, which I have a hunch that it's being used to essentially 'carry' the result of the validation over from the previous iteration. A single iteration can cause it to fail.

I've also noticed a few instructions are kind of pointless. Two lines in every iteration end up functionally being a no-op

mul x 0
add x z

I think if I can just figure out what one iteration is doing, I can figure out what is going on. Or maybe I can just brute-force the individual sections to see what's going on, see if I can figure out a pattern? As long as z=0 by the end of the first section it's fine.

Hmm... interesting...

The 'output' of each one is given by the value of z, and it basically needs to be zero at the end.

The first iteration is just n+3, the second is n+12, third is n+9, etc., so that part seems understandable. Now, I want to see how these iterations interact... I have a feeling that's what the line 5 div difference is going to be. I want to test a hunch... I'm going to test two at a time now.

Curious... for the first two steps, it's a difference of 26.

For the first three, it's 676; aka, 26 * 26.

For the first four, it's... also 676?

For the first five, it's 26 * 26 * 26 = 17,576.

I'm noticing a pattern, in that... hmm... it's... 26 each time... 26 letters of the alphabet..... oh no

The model number is a string. I bet it's a string.

~

Okay. Re-running just the first iteration, but going from a to z this time, seeing what I find.

First step, the values are all sequential from... 20, on..... oh no.

I just tried it with A to Z as well, same result with a different non-intersecting range. Smaller than the lower-case. It's ALL ASCII that it's looking for. Ugh.

~

No wait, this is easier than I thought! Woah! Oh woah, if this works it'll be an amazing twist to this puzzle.

  1. Run each section alone, with the input something like a
  2. Run again, with the input of a minus the output from the previous part.

A quick check of the ASCII table says - should work, so let's see what happens...

W = [-3]        X = [ 1]        Y = [ 0]        Z = [ 0]

Okay, with this methodology, it's easy:

  1. Split the program into 14 individual segments
  2. Run each segment once, with a base input
  3. Subtract the output from the input, store it
  4. Validate the total output against the full program

Okay, I realized that it kind of works for part 3. But also.. just... I dunno, it's probably not ASCII, and 26 is just KIND of a coincidence? It died for the first one with a negative number, so I think I need to see what's going on a bit more...

Ugh. Maybe it's a stack operation of some kind? I just realized exactly half have that line 5 modification. If it's a stack, then the first one must be PUSH, and the ones with the modified line 5 must be POP.

I'll run each position with just the 1 digit, write down the output, and see what results. It may make more sense to do this by hand.

~

On a hunch, I wanted to just see if there would be anything noteworthy if I ran just a few instructions. I noticed a few of them were significatnly smaller starting with just 4 iterations. I don't really see a pattern yet, but now that I can see it.... I can start running through it a bit slower with one of these, to see what's different, and why they're smaller. It's the one that I said must be a POP operation, so... it has to do with the relationship of the third and fourth inputs, right?

It looks like I'm right... as long as digit 4 is 3 greater than digit 3, it goes down, otherwise it doesn't. That holds true for EVERY set.

Okay, now that I've solved this theory, let's test it... immediately after is another push/pop pair. I'll test digits 5 and 6 now.... and a few do stand out! Specifically, where there's a change of -6...

You know, I can probably just brute-force the rules for all of this, and figure it out by hand from there. I know what each pair index is.

 3: 4 | b = a + 3
 5: 6 | b = a - 6
 2: 7 | b = a + 8
10:11 | b = a - 5
 9:12 | b = a + 1
 8:13 | b = a + 5
 1:14 | b = a - 4

Those are the digit pairing rules. The relationships between the digits. As long as those conditions are true, it's valid. I don't quite understand WHY this is the case, but it works.


Oh. Part 2 is just a different answer to those same rules. Not difficult, just follow the rules... easy though.


My solutions for today's puzzles