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.rbefore 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.finditeroverre.findallbecause it returns an iterator whilere.findallreturns 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 theXandYvalues.int(match.group(1)) * int(match.group(2))calculates the product of the two numbers.for match in matchesiterates over the match objects returned byre.finditer.sum(...)calculates the sum of the products of theXandYvalues 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 futuremulinstructions.mul(5,5)is not considered as instructions are disabled.mul(11,8)is not considered as instructions are disableddo()enables futuremulinstructions.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 capturingXandYseparately, 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 = 0Initialize the sum of the products of theXandYvalues.enabled = TrueInitialize theenabledflag toTrueto enable futuremulinstructions, since the memory starts withmulinstructions enabled.for match in matchesIterate over the match objects returned byre.finditer.if match.group(1) and enabled:If the match is amul(X,Y)instruction andenabledisTrue, perform the multiplication. IfenabledisFalse, themulinstruction is ignored, since the most recent conditional must have beendon't(). -a, b = map(int, match.group(1).split(","))Split theX,Yvalues 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!