In day 8 we find a some weird infinite loop on a handheld device. To fix this problem we are asked to create a program that goes trough a stack of instructions.
nop +0
acc +1
jmp +4
acc +3
jmp -3
acc -99
acc +1
jmp -4
acc +6
And after we find where the infinite loop is located we replace the bad command with the correct instruction.
If you want to follow along please download AdventOfCode2020.pbit and check Day 1 post for instructions.
Part 1
To start we need to develop a way of returning the value of the accumulator right before the infinite loop starts. If you haven’t read the instructions on the page here’s a quick recap:
- acc – Increases or decreases the accumulator with it’s argument
- jmp – Jump to the instruction relative to it’s argument
- nop – No Operation, the stack pointer is incremented by 1 and the next operation is executed
After some basic transformations we get to this step:
I have added an index so we can keep track of the visited positions. If
we return to an already visited position we will be in the start of a
infinite loop. Here’s my Power Query
function that returns the value
right after we find the infinite loop:
(t as table) => let maxInstruction = Table.RowCount(t) - 1, listOfInstructions = List.Buffer(t[Instruction]), listOfValues = List.Buffer(t[Value]), runInstructions = List.Generate( () => [CurrentRow=0, Visited={}, Acc=0], each not List.Contains([Visited], [CurrentRow]) and [CurrentRow] <= maxInstruction, each [ CurrentRow = if listOfInstructions{[CurrentRow]} = "jmp" then [CurrentRow] + listOfValues{[CurrentRow]} else [CurrentRow] + 1, Visited = [Visited] & { [CurrentRow] }, Acc = if listOfInstructions{CurrentRow} = "acc" then [Acc] + listOfValues{CurrentRow} else [Acc] ] ), lastLine = List.Last(runInstructions) in [ Value = lastLine[Acc], RanToTheEnd = maxInstruction = lastLine[CurrentRow] ]
We use the List.Generate
to loop each instruction storing the
visited postions in the Visited
list. In each position we check the
instruction and act accordingly adding the current value to the Acc
and adjusting the next visited position.
The return value is a record on which the first position is the value
of the accumulator and the second a boolean. If this last value is
TRUE
we know that we were able to get to the last instruction of
code, consequently there are no infinite loops present.
Part 2
Apparently there is exactly one instruction corrupted. Somewhere in
the program, either a jmp
is supposed to be a nop
, or a nop
is
supposed to be a jmp
.
And what this means is that we need to generate all the possible list of instructions replacing just one instruction on each.
The general idea seems to be:
- Replace one instruction on the original list
- Check if we get to the end
To make a substitution we can use a function like this:
(tt as table, x as number) => let t = Table.Buffer(tt), res = Table.ReplaceValue(t, each [Instruction], each if [Index] = x and [Instruction] = "jmp" then "nop" else if [Index] = x and [Instruction] = "nop" then "jmp" else [Instruction], Replacer.ReplaceText, {"Instruction"}) in res
We receive a table of instructions and the index of the position we want to replace and return the new set of instructions.
Getting the positions of the instructions to replace:
instructionsToReplace = List.Buffer(Table.SelectRows(buffer, each List.Contains({"jmp", "nop"}, [Instruction]))[Index])
And the master loop:
p2Value =
List.Generate(
() => [
Iterator = 0,
CurrentTable = fnRunInstructions(
fnReplaceInstruction(buffer,instructionsToReplace{0})),
Stop = false
],
each not [Stop],
each [
Iterator = [Iterator] + 1,
CurrentTable = fnRunInstructions(
fnReplaceInstruction(buffer,instructionsToReplace{[Iterator]})),
Stop = [CurrentTable][RanToTheEnd]
],
each [CurrentTable]
)
We replace one instruction in the code and run the code. If we get to the end we stop and return the result, otherwise we keep looping.
Conclusion
Another interesting problem with techniques we can reuse when working
with tables of list of movements, e.g. stocks. We can generate
aggregate tables with the values of the stock on the end of month so
that our DAX
is faster. Instead of summing all the movements since
the beginning of times we can just sum since the last aggregate
checkpoint.
Have fun!!