sharick.xyz Home Blog Projects Directory Misc Contact

Advent of Code 2025, Week 1

Tags: Advent of Code

Advent of Code

Advent of Code is a yearly Advent calendar of programming puzzles that's run since 2015, which as of 2025 has 12 two-part problems each year, each part rewarding one "star". For 2025, after completing every previous year, I'm continuing my pattern of making a weekly blog post about that week's problems, containing my thoughts on them and how I solved them; these, of course, will contain spoilers. Although the global leaderboard is no more, I'm still starting each puzzle as soon as the clock strikes midnight, so I'll still be including my solve times for each problem. For the code, see:
2025 ยท personal library.

Day 1: Secret Entrance (puzzle, code)

Solve Times: 02:37, 05:40

I found this problem to be somewhat harder than a typical Day 1 would be, probably due to the compressed schedule requiring a compressed difficulty curve. It presents a dial which can take any value from 0 to 99, and a series of rotations (the input) with a direction and magnitude. The goal is to simulate these rotations, first to find how many times the dial stops at position 0, and then to find how many times the dial is in position 0 at all regardless of whether its rotation is complete.

In part 1, all I needed to do was run each rotation in sequence, using the modulo operator after each one, and use a counter to track how often it lands on position 0. For part 2, I initially attempted to simulate turning the dial up to 100 steps at once in order to count the total number of passes through 0, but had a typo that caused it to fail on large left turns. Instead of fixing it, I quickly pivoted to a less clever approach that rotated by one value at a time, which didn't present any runtime issues due to how small the numbers involved are.

Day 2: Gift Shop (puzzle, code)

Solve Times: 02:58, 06:29

This puzzle is also a bit harder than Day 2 has historically been, although still quite easy overall. This problem's input is a long list of ranges of positive integers, representing intervals of IDs where some are valid and some are invalid. Invalid IDs are defined as any ID that can be split in half into two equal parts, such as 12521252, and the goal is to find the sum of all invalid IDs in all of the ranges. Part 2 expands the definition of an invalid ID to be any ID made of the same sequence of digits repeated, such as 181181181, and asks the same question.

Part 1 was very simple to solve: I just iterated over every value in every range, converted it into a string, split the string in half, and checked if the two halves were equal, incrementing a counter if they were. (The ranges in the input never overlap, so I didn't need any logic to check for duplicates.) In part 2, I opted for a brute force approach of checking all possible substring lengths that divided the ID's length equally, rather than anything more streamlined; like Day 1, the problems this early aren't generally built to prevent brute force approaches like this from working. Thanks to Python's ability to multiply strings, the actual check for a given divisor is very simple to run.

Day 3: Lobby (puzzle, code)

Solve Times: 03:24, 16:23

This is a classic "scaling" problem, where part 1 presents a fairly simple task that can easily be solved using a naive brute force approach, and part 2 increases the size of the problem to the point where brute force is intractable, forcing a more efficient solution. The input in this problem is a list of battery banks (strings of digits) in which all the batteries start "off", and the goal is to maximize the "joltage" of each battery bank by turning exactly two batteries in it on; the joltage produced is equal to the number made by combining these digits in order. In part 2, the number of batteries to turn on in each bank is increased from 2 to 12, which increases the total number of cases a brute force needs to consider by a factor of approximately 5e19.

I immediately saw the naive approach on part 1 and implemented it, checking all possible pairs of batteries on each bank and taking the largest one. Unsurprisingly, this was not viable for part 2, so I decided on a greedy algorithm instead, taking advantage of the fact that, due to the place value system, it's always better to take a larger digit over a smaller one as long as there are enough digits left to reach the total of 12 afterwards. After some difficulties getting the logic right (particularly for handling the final digit correctly), I was able to implement this approach successfully.

Day 4: Printing Department (puzzle, code)

Solve Times: 04:05, 05:40

Day 4's problem is this year's first appearance of another AoC classic: the two-dimensional grid. The cells on this grid are either empty (represented by a .) or contain a roll of paper (represented by a @). Each roll of paper can be removed as long as it neighbors three or less other rolls of paper in the eight adjacent cells. In part 1, the problem only asks how many rolls of paper can be removed in the initial state, but part 2 demands that the actual removal process is simulated until no more removable rolls of paper exist.

My solving process was greatly aided by the grid module in my personal library, specifically the grid_neighbors function, which returns all the neighbors for a point on a grid and automatically drops points located outside the grid's bounds. With this function in hand, all I had to do was iterate through every point on the grid that contained a roll of paper, get its list of neighbors, and count how many of them also contained rolls of paper. Part 2 was somewhat more involved, since it required mutating the grid during iteration; however, since this problem's scenario is very simple and doesn't have any reason that an incorrect mutation would be difficult to debug or cause cascading issues, I didn't have to worry about the typical pitfalls of mutation. Because of this, I was able to just wrap my existing loop through the grid in another loop in order to run it until no more removable rolls existed, and add a line to remove each roll when it was found.

Day 5: Cafeteria (puzzle, code)

Solve Times: 04:11, 22:30

This problem features the return of ranges as the primary type of data. Unlike in Day 2, the ranges in this puzzle can and do overlap. In part 1, the ranges in the input are paired with a list of individual ID numbers, and the goal is to find how many of these individual IDs fall in any of the given ranges. In part 2, these individual IDs are ignored, and instead all of the ranges need to be combined into a smaller set of non-overlapping ranges, in order to find the total amount of numbers that fall within any range.

I solved part 1 very easily: all I had to do was check each individual ID against all the ranges in the list, breaking out of the loop if any of them matched, and count how many IDs matched at the end. Part 2, however, is considerably more complicated, requiring logic to merge the ranges because of how large the numbers involved are. I was hoping that my ranges library would allow me to avoid figuring out the details of merging, but I found that I never wrote that function, so all I could do with it was check for overlaps easier. Using this, I wrote some fairly spaghetti code that repeatedly tries to merge each range with every other range in the list, until it is unable to find any more mergeable range pairs. In retrospect, it would have been much simpler to sort the list first, and write my code in order to keep it sorted. I also tried to reuse my code from 2016 day 20, which also involves merging ranges, but that problem's requirements were different enough that it was easier to start fresh.