Advent of Code 2025 Solutions
December 1, 2025Advent of Code is an annual programming challenge in the month of December. I highly recommend you check it out—the puzzles tend to be very creative and well-designed. This year, there are 12 daily puzzles.
I’ll be updating this blog post with my solutions for each of the puzzles.
There will be spoilers.
Please do not look at this page until you have made an honest effort to complete the puzzles yourself.
My implementations for this and prior years are available on GitHub.
One final note: My solutions are not guaranteed to be optimal, efficient, or even a little bit clever. I enjoy AOC because it helps me improve as a programmer. If you think any of my implementations could be improved, please let me know.
Now onto the puzzles.
Day 1
Python
This puzzle requires us to track the state of a circular dial with numbers 0-99.
Since we want to represent the physical state of an object, let’s make a class.
A Dial will simply store current value of the dial.
class Dial:
def __init__(self):
self.val = 50 # The starting positionWe can then add methods to represent turning the dial left and right.
I think it is preferable to have each method turn the dial exactly one position in the appropriate direction.
This makes the roll-over (99 <-> 0) in each direction very obvious,
and we can process any instruction by repeatedly calling one of these two methods.
class Dial:
def __init__(self):
self.val = 50
def left(self) -> None:
self.val = self.val - 1 if self.val else 99
def right(self) -> None:
self.val = self.val + 1 if self.val < 99 else 0
def turn(self, instr: str) -> None:
direction = instr[0]
amount = int(instr[1:])
for _ in range(amount):
if direction == 'L':
self.left()
else:
self.right()For Part 1 of the problem, we need to count the number of times the dial is at 0 after processing an instruction.
First, we’ll add a field to the class, zeros, which tracks this count.
Since each call of turn corresponds to one instruction, we just need to update zeros after exiting the loop.
def turn(self, instr: str) -> None:
direction = instr[0]
amount = int(instr[1:])
for _ in range(amount):
if direction == 'L':
self.left()
else:
self.right()
if self.val == 0:
self.zeros += 1Solving Part 1 is thus very simple. We instantiate a new Dial, and loop through the instructions:
def count_zeros(instrs: Iterable[str]) -> int:
d = Dial()
for instr in instrs:
d.turn(instr)
return d.zerosThe twist in Part 2 is that we need to count every time the dial reaches or passes over 0.
The decision to decompose each instruction into a series of individual rotations makes this really easy.
In fact, we just need to move the value check in turn inside the loop.
def turn(self, instr: str) -> None:
direction = instr[0]
amount = int(instr[1:])
for _ in range(amount):
if direction == 'L':
self.left()
else:
self.right()
-> if self.val == 0:
-> self.zeros += 1That’s it!
Day 2
Python
This puzzle involves discerning “invalid” numbers from a list of integer ranges. For Part 1, a number is “invalid” if it consists of some sequence of digits repeated twice.
So, for arbitrary digits A and B, the numbers AA, ABAB, ABAABA, etc. are invalid.
Since we’re concerned with the structure of digits in a string, this immediately strikes me as a regular expression problem. I’m sure there’s an efficient mathematical way to determine if a number fits the criterion, but regular expressions are much closer to natural language, and are thus easier to implement.
Here’s the regex we need:
^(\d+)\1$
Let’s break it down.
^: Anchor to the beginning of the string(\d+): Match one or more numerical digits (\dis the same as[0-9]). We create a capturing group by wrapping this subexpression in parentheses. This is basically a note to the regex engine to keep track of whatever was matched here, so we can refer to it later.\1: A backreference to the first (and only) capturing group we defined. This will evaluate to the value matched by the preceding(\d+).$: Anchor to the end of the string.
Putting all those together, we represent a string which consists entirely of a sequence of one or more digits, followed by that same sequence again.
All we need are a few helper functions. First we’ll parse the input.
def parse_input(s: str) -> list[list[int]]:
return [[int(n) for n in rng.split("-")] for rng in s.split(",")]Then, we’ll filter out all of the valid numbers, and add up everything left.
double_pattern = re.compile(r"^(\d+)\1$")
def add_invalid_in_range(start: int, end: int, pattern: re.Pattern) -> int:
return sum(i for i in range(start, end + 1) if re.match(pattern, str(i)))Note that we use re.match to check if our string fits the pattern.
Python has a handful of similarly-named functions, and it’s important to use the right one.
re.match will be truthy if the given string starts with a sequence that matches the pattern.
It’s OK to use it here because the regex pattern is anchored to the beginning and end of the string.
If you didn’t include the ^...$ anchors, you would need to use re.fullmatch instead.
The documentation includes a writeup explaining the difference between these functions and re.search.
Finally, we just need to call add_invalid_in_range on every single range.
def sum_invalid(s: str, pattern: re.Pattern) -> int:
return sum(add_invalid_in_range(*rng, pattern) for rng in parse_input(s))This is the solution to Part 1! Regex lets us solve it with three one-liners and a pattern.
Part 2 requires us to expand the criterion for invalid items. Now, a number is invalid if it consists of two or more repetitions of a sequence of digits.
This is trivial to implement, because regular expressions are so great at this kind of quantifications. In fact, we just need to add a single character to the original pattern:
^(\d+)\1+$
^
The + quantifier means we’ll match one or more extra instances of the original sequence. Voilà.
As a final note, this solution is not particularly efficient. Regex is slow. But it is a very powerful tool for reasoning about the structure of text, and for converting high-level descriptions into actionable programs.
Day 3
Python
Today’s puzzle is again digits-based. Given a string of digits, we have to find the maximum two-digit number than can be made by selecting digits left-to-right. The greatest number that can be made with the string 9582 is 98, but the maximum of 8592 is 92.
The key observation is that we need to maximize the first digit. Once we do that, we can select the greatest value from all digits to the right of the first one we picked.
Finding the index of the greatest value in a list is a function called argmax, which is commonly used in machine learning. Here’s a Pythonic implementation thereof:
def argmax(lst: list[int]) -> tuple[int, int]:
"""Returns the index and the value of the greatest element in the list."""
return max(enumerate(lst), key = lambda x: x[1])enumerate(...) returns a list of (index,value) tuples, within which we find the tuple that has the maximum value, and return it. Note that we’re returning both the index and the maximum value, because we’ll need both to solve the puzzle.
We can use argmax to find the greatest number to use as the tens digit. It is important that we do not search the entire list of values, because we will still need to find the ones digit. What if our input was something like 529? The maximum value is 9, but because this is the last value, it could only ever be the ones digit. Here’s my implementation of Part 1:
def max_two_digits(digits: list[int]) -> int:
idx, left = argmax(digits[:-1])
_, right = argmax(digits[idx + 1:])
result = 10 * left + right
return resultThe first line finds the left (tens) digit; note that it excludes the final value as explained above. Once the left digit has been found, the list is partitioned right after it. We take the argmax again to find the right (ones) digit, but this time only search the portion of the list after the index of the left digit.
Finally we just need a couple of helper functions to parse the input from text to lists of integers, and add up all the joltages:
def parse_file(f):
"""Splits the input file into lists of integers."""
for line in f:
yield [int(n) for n in line.strip()]
def sum_joltages(f, n:int, verbose=False) -> int:
"""Finds the total joltage for a file-like containing battery levels."""
return sum(max_two_digits(digits) for digits in parse_file(f))That’s it for Part 1!
Part 2 predictably asks us to generalize from two-digit numbers. It specifically asks us for 12 digits, but we’ll implement a function that works for all numbers >1.
This isn’t particularly difficult because we were already very close. The procedure for max_two_digits is just to call argmax, then shrink the list and call argmax again.
To generalize, we just have to make this a recursive function. Call argmax, then shrink the list and recur. There are a few tricky points to discuss, but here’s the generalized solution for Part 2:
def max_n_digits(digits: list[int], n: int) -> int:
"""
Returns the greatest n-digit number that can be constructed
from the provided list of digits, left-to-right.
"""
n_1 = n - 1
right_bound = -n_1 or None
idx, digit = argmax(digits[:right_bound])
if n == 1:
return digit
else:
return (digit * 10 ** n_1) + max_n_digits(digits[idx + 1:], n_1)right_bound represents the indices to reserve from each search for the maximum digit. When we’re looking for the first of 12 digits, we know for certain that it won’t be within the last 11 (n - 1) values in the list. In the base case where n = 1, we don’t want to exclude any values. Slicing a list at [:-0] will give us the empty list, so we need to ensure that the end index is None in this case.
The sum is built up by adding the maximum digit mulitplied by \(10^{n-1}\), which corresponds to the appropriate place-value.
A speedy and elegant solution for a somewhat simple puzzle. Because Advent of Code is only 12 days this year, I suspect the difficulty will start ramping up soon.