Day 3: Mull It Over

Author

Kasia Kedzierska

Published

December 3, 2024

Modified

December 5, 2024

The next place we search for the Chief Historian is the North Pole Toboggan Rental Shop. While here, we are tasked with fixing the computers – we need to clean the input for the multiplication.

For the full details of the task, you can visit the official Advent of Code 2024 website.

To set the scene, I again asked ChatGPT to generate an image of the shop. Here’s the result:

Part 1: Extracting multiplication instructions

The program the shopkeeper is using is supposed to perform multiplication, but it’s not working as expected. The input is a string that contains a series of multiplication instructions, but since it is corrupted there are also many invalid characters. The goal is to extract these instructions and calculate the product of the two numbers in each instruction.

A correct multiplication instruction is formatted as follows: mul(X,Y), where X and Y are 1 to 3 digit integers. The goal is to extract these instructions and calculate the sum of the products of the two numbers in each instruction.

For the example:

xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))

The correct instructions are mul(2,4), mul(5,5), mul(11,8), and mul(8,5). The sum of the products of the two numbers in each instruction is 8 + 25 + 88 + 40 = 161.

from misc.helper import verify_answer

example_input = """xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))"""

example_answer_one = 161

Today, I will use a consise Python code to solve this problem using the re module for regular expressions.

import re
from pathlib import Path


def part_one(input: str | Path):
    if Path(input).exists():
        with open(input, "r") as file:
            input = file.read()

    def extract_matches(input):
        return map(multiply, re.findall(r"mul\((\d{1,3}),(\d{1,3})\)", input))

    def multiply(tup: tuple[str, str]) -> int:
        return int(tup[0]) * int(tup[1])

    return sum(extract_matches(input))


verify_answer(part_one, example_input, example_answer_one)
✔️ That's right! The answer is 161.
%time
part_one("../inputs/day_03.txt")
CPU times: user 2 μs, sys: 0 ns, total: 2 μs
Wall time: 8.82 μs
196826776

That’s the right answer! You are one gold star ⭐ closer to finding the Chief Historian.

Part 2: Extracting multiplication instructions with a twist

This time, we are also looking out for do() and don't() instructions that modify the next valid multiplication instruction. If we encounter a do() instruction, we should multiply the two numbers in the next valid multiplication instruction. If we encounter a don't() instruction, we should skip the next valid multiplication instruction. At the start of the seuence, the multiplications are enabled.

For example:

xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))

We have the following sequence:

.mul(2,4)...........don't().mul(5,5)............mul(11,8).do().mul(8,5).

Which means, that:

  • we multiply 2*4 = 8,
  • we encounter don't(), so we skip the next multiplications mul(5,5) and mul(11,8),
  • until we again encounter do(), so we multiply 8*5 = 40.

The sum of the products of the two numbers in each instruction is 8 + 40 = 48.

example_input_two = """xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))"""

example_answer_two = 48
def part_two(input: str | Path):
    if Path(input).exists():
        with open(input, "r") as file:
            input = file.read()

    def extract_matches(input):
        return re.findall(
            r"(do\(\))|mul\((\d{1,3}),(\d{1,3})\)|(don't\(\))", input
        )

    def multiply(tup: tuple[str, str]) -> int:
        return int(tup[0]) * int(tup[1])

    go = True
    sum = 0
    for match in extract_matches(input):
        if match[0]:
            go = True
        elif match[3]:
            go = False
        else:
            if go:
                sum += multiply(match[1:3])

    return sum

Now, let’s test the code on the examples.

verify_answer(part_two, example_input_two, example_answer_two)
✔️ That's right! The answer is 48.
verify_answer(part_two, example_input, example_answer_one)
✔️ That's right! The answer is 161.

Since both test cases work, I will run the code on the actual input.

%time
part_two("../inputs/day_03.txt")
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 1.91 μs
106780429

That’s the right answer! You are one gold star ⭐ closer to finding the Chief Historian.

Bonus Bash solutions

I wanted to see how the same problems can be solved in Bash. The full script is available in the Day03.sh file in the repository. Here’s a brief overview of the Bash solutions:

In part 1, I extract only the multiplication instructions and calculate the sum of the products.

part_one() {
  local input="${1:-/dev/stdin}"

  cat "${input}" |
    grep -o 'mul([0-9]\{1,3\},*[0-9]\{1,3\})' |
    sed 's/mul(//g;s/)//g;s/,/*/g' |
    xargs |
    sed 's/ /+/g' |
    bc
}

I use grep to extract the multiplication instructions, sed to format them as num1*num2, xargs to join them into a single line, and bc to evaluate the sum.

The second part is a bit more complex, as it requires handling the do() and don't() instructions.

part_two() {
  local input="${1:-/dev/stdin}"

  cat "${input}" |
    grep -o "mul([0-9]\{1,3\},*[0-9]\{1,3\})\|don't()\|do()" |
    sed 's/mul(//g;s/)//g;s/,/*/g;s/(//g' |
    awk '
BEGIN { go = 1 }
{
    if ($1 == "don'\''t") {
        go = 0
    } else if ($1 == "do") {
        go = 1
    } else if (go) {
        print $1
    }
}' |
    xargs |
    sed 's/ /+/g' |
    bc
}

Here, on top of grep and sed to extract the instructions, I use awk to handle the do() and don't() instructions.

In the script I also added functions to check the answers and print them with cowsay for some extra flavor. Like this:

  _                 
 |_) _. ._ _|_   /| 
 |  (_| |   |_    | 
                    
 _______________________________ 
< Part 1 - example test passed! >
 ------------------------------- 
   \
    \
        .--.
       |o_o |
       |:_/ |
      //   \ \
     (|     | )
    /'\_   _/`\
    \___)=(___/

 _____________________________ 
< Part 1 answer is: 196826776 >
 ----------------------------- 
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||