Categories
Programming What I’m Up To

My solution to Advent of Code 2020’s Day 8 challenge, in Python

Welcome to another installment in my Advent of Code 2020 series, where I present my solutions to this year’s Advent of Code challenges!

In this installment, I share my Python solution to Day 8 of Advent of Code, titled Handheld Halting.

Spoiler alert!

Please be warned: If you want to try solving the challenge on your own and without any help, stop reading now! The remainder of this post will be all about my solution to both parts of the Day 8 challenge.

The Day 8 challenge, part one

The Kittenbot Meowbit handheld game console. Tap to find out more.

The challenge

Here’s the text from part one of the challenge:

Your flight to the major airline hub reaches cruising altitude without incident. While you consider checking the in-flight menu for one of those drinks that come with a little umbrella, you are interrupted by the kid sitting next to you.

Their handheld game console won’t turn on! They ask if you can take a look.

You narrow the problem down to a strange infinite loop in the boot code (your puzzle input) of the device. You should be able to fix it, but first you need to be able to run the code in isolation.

The boot code is represented as a text file with one instruction per line of text. Each instruction consists of an operation (accjmp, or nop) and an argument (a signed number like +4 or -20).

  • acc increases or decreases a single global value called the accumulator by the value given in the argument. For example, acc +7 would increase the accumulator by 7. The accumulator starts at 0. After an acc instruction, the instruction immediately below it is executed next.
  • jmp jumps to a new instruction relative to itself. The next instruction to execute is found using the argument as an offset from the jmp instruction; for example, jmp +2 would skip the next instruction, jmp +1 would continue to the instruction immediately below it, and jmp -20 would cause the instruction 20 lines above to be executed next.
  • nop stands for No OPeration – it does nothing. The instruction immediately below it is executed next.

For example, consider the following program:

These instructions are visited in this order:

First, the nop +0 does nothing. Then, the accumulator is increased from 0 to 1 (acc +1) and jmp +4 sets the next instruction to the other acc +1 near the bottom. After it increases the accumulator from 1 to 2, jmp -4 executes, setting the next instruction to the only acc +3. It sets the accumulator to 5, and jmp -3 causes the program to continue back at the first acc +1.

This is an infinite loop: with this sequence of jumps, the program will run forever. The moment the program tries to run any instruction a second time, you know it will never terminate.

Immediately before the program would run an instruction a second time, the value in the accumulator is 5.

Run your copy of the boot code. Immediately before any instruction is executed a second time, what value is in the accumulator?

Importing the data

Every Advent of Code participant gets their own set of data. I copied my data and went through my usual process of bringing it into a Jupyter Notebook running a Python kernel.

This involves pasting it into a triple-quoted string and then assigning its value to the variable main_raw_input:

Building the data structure

With the data imported, the next step was to build an appropriate data structure. Since today’s puzzle was based on simplified microprocessor instructions, I thought that the data structure should do the same.

I wanted my data structure to look like the table below, which shows the instructions from extracted from the first five lines of the data given to me:

Index Opcode Operand
0 acc 37
1 acc -4
2 nop 405
3 jmp 276
4 acc 39

Here’s the function that I wrote to convert the input data into the data structure:

With the function defined, here’s how I used it to create the data structure, which I assigned to a variable named instructions:

I then wrote accumulator_value_at_first_repeat(), the function that would use the data structure contained within instructions to solve the first challenge:

When run, it sets up the following variables:

  • program_length: The number of instructions in the data structure.
  • program_counter: The program’s equivalent of the “program counter (PC)” register in a microprocessor; contains the index the instruction to be executed.
  • accumulator: The program’s equivalent of an accumulator in a microprocessor. For those of you who aren’t familiar with what happens at the microprocessor level, think of the accumulator as a “scratchpad” variable where you do all the math.
  • executed_instructions: A set (which means that every value in it is unique) of the indices of all the instructions that have already been executed. We use it to determine if a given instruction has already been run, at which point we should stop the program and see what the value in the accumulator is.
  • repeat_instruction_encountered: A flag that should be set to True when an instruction is about to be executed a second time.

The function’s main loop performs a greatly simplified version of a microprocessor’s “fetch-decode-execute” cycle:

  1. Fetch the current instruction, which is the one whose index is specified by program_counter.
  2. See if the instruction has been executed before. If this is the case, exit the loop; otherwise, recording this instruction as having been executed.
  3. Decode the current instruction:
    • If the instruction is jmp, add the operand value to program_counter and go back to the start of the loop.
    • If the instruction is acc, add the operand value to accumulator.
    • If the instruction is nop, do nothing.
    • If the instruction is anything else, display an error message.
  4. After the loop is done, if the repeat_instruction_encountered flag is set, we’ve found the value we’re looking for — display it! Otherwise, display a message saying that we’ve reached the end of the instructions without ever repeating one.

I ran the function…

…and got my result: 1801.

The Day 8 challenge, part two

The TinkerGen GameGo programmable handheld game console. Tap to find out more!

The challenge

After some careful analysis, you believe that exactly one instruction is corrupted.

Somewhere in the program, either a jmp is supposed to be a nopor a nop is supposed to be a jmp. (No acc instructions were harmed in the corruption of this boot code.)

The program is supposed to terminate by attempting to execute an instruction immediately after the last instruction in the file. By changing exactly one jmp or nop, you can repair the boot code and make it terminate correctly.

For example, consider the same program from above:

If you change the first instruction from nop +0 to jmp +0, it would create a single-instruction infinite loop, never leaving that instruction. If you change almost any of the jmp instructions, the program will still eventually find another jmp instruction and loop forever.

However, if you change the second-to-last instruction (from jmp -4 to nop -4), the program terminates! The instructions are visited in this order:

After the last instruction (acc +6), the program terminates by attempting to run the instruction below the last instruction in the file. With this change, after the program terminates, the accumulator contains the value 8 (acc +1acc +1acc +6).

Fix the program so that it terminates normally by changing exactly one jmp (to nop) or nop (to jmp). What is the value of the accumulator after the program terminates?

Here’s the function I wrote to solve this challenge:

The function, accumulator_value_and_halt_at_first_repeat(), is an expanded version of the function from part one, accumulator_value_at_first_repeat().

In addition to a set of instructions, it takes an additional parameter: the address of an instruction that should be changed — either from jmp to nop, or from nop to jmp.

The function still performs the “fetch-decode-execute” loop, and it exits the loop if it’s about to execute an instruction that’s already been executed. The main difference is that if the current instruction is the one flagged for change, it changes the instruction appropriately.

I wrote the accumulator_value_and_halt_at_first_repeat() function to be used by the function below:

This function goes through all the instructions in the set, looking for any jmp or nop instructions. When it finds one, it runs the program using accumulator_value_and_halt_at_first_repeat(), marking the jmp or nop instruction as the one to be changed.

This lets us modify the program, one jmp or nop instruction at a time, in order to find which change to a jmp or nop instruction is the one that allows the program to reach the end of the instructions.

Here’s an abridged version of what happened when I ran the function:

I entered 2060 as my answer, and step two was complete.

Solutions for other days in Advent of Code 2020

Categories
Programming What I’m Up To

My solution to Advent of Code 2020’s Day 6 challenge, in Python

Welcome to another installment in my Advent of Code 2020 series, where I present my solutions to this year’s Advent of Code challenges!

In this installment, I share my Python solution to Day 6 of Advent of Code, titled Custom Customs.

Spoiler alert!

Please be warned: If you want to try solving the challenge on your own and without any help, stop reading now! The remainder of this post will be all about my solution to both parts of the Day 6 challenge.

The Day 6 challenge, part one

The challenge

Here’s the text from part one of the challenge:

As your flight approaches the regional airport where you’ll switch to a much larger plane, customs declaration forms are distributed to the passengers.

The form asks a series of 26 yes-or-no questions marked a through z. All you need to do is identify the questions for which anyone in your group answers “yes”. Since your group is just you, this doesn’t take very long.

However, the person sitting next to you seems to be experiencing a language barrier and asks if you can help. For each of the people in their group, you write down the questions for which they answer “yes”, one per line. For example:

In this group, there are 6 questions to which anyone answered “yes”: abcxy, and z. (Duplicate answers to the same question don’t count extra; each question counts at most once.)

Another group asks for your help, then another, and eventually you’ve collected answers from every group on the plane (your puzzle input). Each group’s answers are separated by a blank line, and within each group, each person’s answers are on a single line. For example:

This list represents answers from five groups:

  • The first group contains one person who answered “yes” to 3 questions: ab, and c.
  • The second group contains three people; combined, they answered “yes” to 3 questions: ab, and c.
  • The third group contains two people; combined, they answered “yes” to 3 questions: ab, and c.
  • The fourth group contains four people; combined, they answered “yes” to only 1 question, a.
  • The last group contains one person who answered “yes” to only 1 question, b.

In this example, the sum of these counts is 3 + 3 + 3 + 1 + 1 = 11.

For each group, count the number of questions to which anyone answered “yes”. What is the sum of those counts?

Importing the data

Every Advent of Code participant gets their own set of data. I copied my data and went through my usual process of bringing it into a Jupyter Notebook running a Python kernel.

This involves pasting it into a triple-quoted string and assigning it to the variable raw_input, and then splitting it using two newline characters in a row as a delimiter, producing a list named split_input:

Each item in the split_input list represents the collected answers for a group. If a group has more than one person in it, a newline character separates each person’s answers.

In the sample of split_input shown below:

  • The first line shows the answers for the first group, which is made up of two people.
  • The fifth line shows the answers for the fifth group, which is made up of five people. All of them answered yes to only one question: Question v.

Finally, I split each item in split_items, using the newline character as the separator:

The result was a list that I named groups, where:

  • Each item in groups represents a group of passengers
  • Each group is a list, where each item in the list represents the answers for one passenger in that group.

Here’s a sample of groups:

With the input data massaged into something that could easily be processed in Python, it was time to get to work.

Strategy

The goal was to get the total of all the “yes” answers for all the groups, keeping in mind that if any person in the group answers “yes” to a given question, the group is considered to have answered “yes” to that question.

Consider a group of three people. If:

  • the first person in the group answered “yes” to questions a, b, and c,
  • the second person in the group answered “yes” to questions d, e, and f,
  • and the third person in the group answered “yes” to questions g, h, and i…

…the the group is considered to have answered yes to questions a though i.

To put it mathematically, a group’s answers was the union of the answers of everyone in the group.

With that in mind, I wrote this function:

This function takes advantage of the fact that while Python’s union() is a set method, it can take one or more non-set arguments, as long as it can convert the arguments into a set.

For example, this code:

results in this set:

I rearranged the set so that its items would appear in alphabetical order so that it would be easier to read. This is fine, because with sets, there is no order.

Now that I had count_union_group_answers(), I could apply it to groups

…and get the answer for part one: 6504.

The Day 6 challenge, part two

The challenge

Here’s the text of part two:

As you finish the last group’s customs declaration, you notice that you misread one word in the instructions:

You don’t need to identify the questions to which anyone answered “yes”; you need to identify the questions to which everyone answered “yes”!

Using the same example as above:

This list represents answers from five groups:

  • In the first group, everyone (all 1 person) answered “yes” to 3 questions: ab, and c.
  • In the second group, there is no question to which everyone answered “yes”.
  • In the third group, everyone answered yes to only 1 question, a. Since some people did not answer “yes” to b or c, they don’t count.
  • In the fourth group, everyone answered yes to only 1 question, a.
  • In the fifth group, everyone (all 1 person) answered “yes” to 1 question, b.

In this example, the sum of these counts is 3 + 0 + 1 + 1 + 1 = 6.

For each group, count the number of questions to which everyone answered “yes”. What is the sum of those counts?

Strategy

This time, the goal was to get the total of all the “yes” answers for all the groups, keeping in mind that the group is only considered to have answered “yes” to a given question if every person in the group answered “yes” to that question.

Consider a group of three people. If:

  • the first person in the group answered “yes” to questions a, b, and c,
  • the second person in the group answered “yes” to questions a, e, and f,
  • and the third person in the group answered “yes” to questions a, h, and i…

…the the group is considered to have answered yes to question a, and nothing else.

To put it mathematically, a group’s answers was the intersection of the answers of everyone in the group.

All I had to do was tweak the count_union_group_answers() function from part one to find the intersection of group members’ answers…

…and then apply count_intersection_group_answers() to groups

This gave me the answer for part two: 3351.

Recommended reading

Solutions for other days in Advent of Code 2020

Categories
Programming What I’m Up To

My solution to Advent of Code 2020’s Day 5 challenge, in Python

Welcome to another installment in my Advent of Code 2020 series, where I present my solutions to this year’s Advent of Code challenges!

In this installment, I share my Python solution to Day 5 of Advent of Code, titled Binary Boarding.

Spoiler alert!

Please be warned: If you want to try solving the challenge on your own and without any help, stop reading now! The remainder of this post will be all about my solution to both parts of the Day 5 challenge.

The Day 5 challenge, part one

The challenge

Here’s the text from part one of the challenge:

You board your plane only to discover a new problem: you dropped your boarding pass! You aren’t sure which seat is yours, and all of the flight attendants are busy with the flood of people that suddenly made it through passport control.

You write a quick program to use your phone’s camera to scan all of the nearby boarding passes (your puzzle input); perhaps you can find your seat through process of elimination.

Instead of zones or groups, this airline uses binary space partitioning to seat people. A seat might be specified like FBFBBFFRLR, where F means “front”, B means “back”, L means “left”, and R means “right”.

The first 7 characters will either be F or B; these specify exactly one of the 128 rows on the plane (numbered 0 through 127). Each letter tells you which half of a region the given seat is in. Start with the whole list of rows; the first letter indicates whether the seat is in the front (0 through 63) or the back (64 through 127). The next letter indicates which half of that region the seat is in, and so on until you’re left with exactly one row.

For example, consider just the first seven characters of FBFBBFFRLR:

  • Start by considering the whole range, rows 0 through 127.
  • F means to take the lower half, keeping rows 0 through 63.
  • B means to take the upper half, keeping rows 32 through 63.
  • F means to take the lower half, keeping rows 32 through 47.
  • B means to take the upper half, keeping rows 40 through 47.
  • B keeps rows 44 through 47.
  • F keeps rows 44 through 45.
  • The final F keeps the lower of the two, row 44.

The last three characters will be either L or R; these specify exactly one of the 8 columns of seats on the plane (numbered 0 through 7). The same process as above proceeds again, this time with only three steps. L means to keep the lower half, while R means to keep the upper half.

For example, consider just the last 3 characters of FBFBBFFRLR:

  • Start by considering the whole range, columns 0 through 7.
  • R means to take the upper half, keeping columns 4 through 7.
  • L means to take the lower half, keeping columns 4 through 5.
  • The final R keeps the upper of the two, column 5.

So, decoding FBFBBFFRLR reveals that it is the seat at row 44, column 5.

Every seat also has a unique seat ID: multiply the row by 8, then add the column. In this example, the seat has ID 44 * 8 + 5 = 357.

Here are some other boarding passes:

  • BFFFBBFRRR: row 70, column 7, seat ID 567.
  • FFFBBBFRRR: row 14, column 7, seat ID 119.
  • BBFFBBFRLL: row 102, column 4, seat ID 820.

As a sanity check, look through your list of boarding passes. What is the highest seat ID on a boarding pass?

Importing the data

Every Advent of Code participant gets their own set of data. I copied my data and went through my usual process of bringing it into Python. This involves pasting it into a triple-quoted string and assigning it to the variable raw_input, and then splitting it using the newline character as a delimiter, producing a list named split_input:

Strategy

They dropped a hint in the title of the challenge: Binary Boarding.

They dropped a hint by saying the airlines seats people using binary space partitioning instead of zones.

They dropped a hint when they said that the row numbers range from 0 to 127, and that a seven-character sequence determined your row. Only two characters could be used in that sequence, F and B.

(Additional hint for those of you who didn’t study binary numbers: The numbers 0 through 127 can be represented using seven bits.)

They dropped a hint when they said that the seat numbers (which they called column numbers) range from 0 to 7, and that a three-character sequence determined your seat. Only two characters could be used in that sequence, L and R.

(Additional hint for those of you who didn’t study binary numbers: The numbers 0 through 7 can be represented using three bits.)

The “FB” and “LR” sequences are just binary numbers in disguise.

Solving this challenge involves:

  • Converting the “FB” and “LR” strings into binary strings.
  • Converting those binary strings into decimal numbers.
  • Doing the math on those numbers to get the seat IDs.

Converting the “FB” and “LR” strings into binary strings and converting those binary strings into decimal numbers

For each “FB” string, I wanted to convert the Fs to 0s and Bs to 1s. In order to do this, I used Python’s string.maketrans() method to build a character translation table and its string.translate() method to make the translation using that table.

The result was this function:

The first line in the function uses string.maketrans() to define a translation table. string.maketrans() takes two arguments:

  1. The characters to be translated.
  2. The corresponding resulting characters.

In this case, I only want F to be translated into 0 and B to be translated into 1. All other characters translated using this table will remain unchanged.

The second line does the actual translating, using the translation table as its guide.

With the function defined, I could then use it to create a list of all the rows, expressed as binary strings:

This line of code creates a new list, binary_rows, by taking the first seven characters of each line of input data — the “FB” string — and converting it to binary.

Here’s a sample of the result:

The next step was to convert binary_rows into a list of its numeric equivalents: