
Advent of Code 2024 Day 3 – Mull It Over
- 7 minsDay 3: Mull It Over
Part 1
Today’s puzzle is called “Mull It Over”. The input for today’s puzzle is a file where the lines represent corrupted memory, with uncorrupted instructions and corrupted instructions. This is the example input that was given.
xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))
The instructions we need to look for in the memory are mul(X,Y)
where X
and Y
are 1-3 digit numbers. mul(X,Y)
takes two arguments, X
and Y
, and returns the product of X
and Y
. In the example, there are four uncorrupted instructions: mul(2,4)
, mul(5,5)
, mul(11,8)
, and mul(8,5)
.
Our task is to find the sum of the products of the X
and Y
values in the uncorrupted instructions.
My Solution
For my solution, the simplest way that I could think of to extract the uncorrupted instructions is to use a regular expression to match the mul(X,Y)
pattern.
import re
def get_result1(line: str) -> int:
# Search for mul(a,b) in the line,
# Group 1: a, Group 2: b
pattern = r"mul\((\d{1,3}),(\d{1,3})\)"
matches = re.finditer(pattern, line)
return sum(int(match.group(1)) * int(match.group(2)) for match in matches)
Breaking down the code:
pattern = r"mul\((\d{1,3}),(\d{1,3})\)"
This is a regular expression pattern that matches themul(X,Y)
pattern.r
before the string indicates a raw string, which is useful for regular expressions, where backslashes are often used.mul\(
Matches the literal stringmul(
.(\d{1,3})
Matches 1-3 digits and captures them into group 1.,
: Matches the comma separating the two numbers.(\d{1,3})
Matches 1-3 digits and captures them into group 2.\)
Matches the closing parenthesis.
matches = re.finditer(pattern, line)
Find all matches of the pattern in the line, returning an iterator of match objects. I prefer usingre.finditer
overre.findall
because it returns an iterator whilere.findall
returns a list of match objects. Iterators are lazy; i.e. they only return the next element in the sequence or list when asked to do so, which is good when the number of elements are unknown.return sum(int(match.group(1)) * int(match.group(2)) for match in matches)
:match.group(1)
andmatch.group(2)
return the values of the first and second captured groups in the match object, which are theX
andY
values.int(match.group(1)) * int(match.group(2))
calculates the product of the two numbers.for match in matches
iterates over the match objects returned byre.finditer
.sum(...)
calculates the sum of the products of theX
andY
values in the uncorrupted instructions.
I’ve only included the relevant parts of the code here, but to see my full solution, you can check out my Advent of Code GitHub repository.
Part 2
For part 2, there are additional instructions in the corrupted memory that we need to consider.
There are now 2 new instructions to handle:
do()
: This instruction enables futuremul(X,Y)
instructions.don't()
: This instruction disables futuremul(X,Y)
instructions.
At the beginning of the memory, mul
instructions are enabled.
Looking at a new example input:
xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))
Breaking down the instructions in the example:
mul(2,4)
is enabled and is a valid instruction.don't()
disables futuremul
instructions.mul(5,5)
is not considered as instructions are disabled.mul(11,8)
is not considered as instructions are disableddo()
enables futuremul
instructions.mul(8,5)
is enabled and is a valid instruction.- Final sum:
2*4 + 8*5 = 48
.
Our task is to find the sum of the products of the X
and Y
values in the uncorrupted instructions, considering the new conditional instructions.
My Solution
To solve part 2, my regex pattern needs to be updated to handle the new conditional instructions.
pattern = r"mul\((\d{1,3},\d{1,3})\)|(do\(\))|(don't\(\))"

Explanation of the updated pattern:
mul\((\d{1,3},\d{1,3})\)
matches themul(X,Y)
pattern, like in part 1, but instead of capturingX
andY
separately, it captures them together as a single group.|
is the alternation operator, which allows matching either the left or right side of the|
.(do\(\))
matches thedo()
instruction, and captures it into group 2.(don't\(\))
matches thedon't()
instruction, and captures it into group 3.
So this pattern will match either the mul(X,Y)
instruction, do()
, or don't()
.
Now, let’s update the get_result2
function to handle the new instructions.
def get_result2(line: str) -> int:
pattern = r"mul\((\d{1,3},\d{1,3})\)|(do\(\))|(don't\(\))"
matches = re.finditer(pattern, line)
sum = 0
enabled = True
for match in matches:
if match.group(1) and enabled:
a, b = map(int, match.group(1).split(","))
sum += a * b
elif match.group(2):
enabled = True
elif match.group(3):
enabled = False
return sum
Breaking down the code:
pattern = r"mul\((\d{1,3},\d{1,3})\)|(do\(\))|(don't\(\))"
This is the updated regular expression pattern that matches themul(X,Y)
,do()
, anddon't()
instructions.matches = re.finditer(pattern, line)
Find all matches of the pattern in the line, returning an iterator of match objects.sum = 0
Initialize the sum of the products of theX
andY
values.enabled = True
Initialize theenabled
flag toTrue
to enable futuremul
instructions, since the memory starts withmul
instructions enabled.for match in matches
Iterate over the match objects returned byre.finditer
.if match.group(1) and enabled:
If the match is amul(X,Y)
instruction andenabled
isTrue
, perform the multiplication. Ifenabled
isFalse
, themul
instruction is ignored, since the most recent conditional must have beendon't()
. -a, b = map(int, match.group(1).split(","))
Split theX,Y
values and convert them to integers.elif match.group(2):
If the match is ado()
instruction, enable future multiplication.elif match.group(3):
If the match is adon't()
instruction, disable future multiplication.
This code will go through all the instructions in the memory, considering the conditional instructions to calculate the sum of the products of the X
and Y
values in the uncorrupted instructions.
That’s it for day 3 of Advent of Code 2024! I hope you enjoyed reading my solution and let’s see how the rest of the month goes!