Advent of Code 2024
It is December again and that means it is time for the Advent of Code. Due to my workload and family obligations I will probably not be able to get very far this year, but still I wanted to write a post about it.
This year I am using Java, together with my students. My goal is to write as modern as possible Java, which means using streams and new language constructs where possible.
Day 1
In day 1 we are parsing 2 lists of numbers, with the lists printed vertically. This means each line has 2 numbers, one for list one and the other for list two. To parse these data structures I used a very nice stream where I map
each line onto a String[]
using split
.
To be sure that the input is valid, the peek
method allows you to check if the result is what you intended, and otherwise an exception will terminate everything. From here I map
the String[]
into a Pair
record which holds the 2 numbers. Streaming over the resulting pairs
the left and right lists can be extracted quite easily.
I loved this approach, it is very straightforward and does not have a lot of control flow.
Solving the problem with these lists was quite easy. In part 2 there was a need for a frequency table of a list. I also found a very nice solution to that problem using the groupingBy
method from Collectors
.
Day 2
I really liked day 2, the first part was quite straightforward. You have to identify increment or decrement only lists and apply some conditions to them.
Part 2 was much more interesting, here you have to account for fault tolerance. In the Python implementations that were posted the common solution is to concatenate 2 parts of the array and then rerun the validation logic.
Using streams we can something very similar. First we use an IntStream
to iterate over every int[]
(report). Then for every int
in that report, we construct a new array by filtering
out the index of the current item. After that it is a simple case of determining increment or decrement and applying the conditional logic.
Suppose you have a list of
[1,2,4,7]
, while iterating it will first hit index0
, thefilter
will prevent that entry from continuing. Next1
through3
will continue and as a result oftoArray()
a new array will be constructed with only those items.
My first solution was nothing like this, but after refining it I am very happy with how clean it came out.
Day 3
This was the traditional easy puzzle after a more complicated one. Basically simple parsing for which I used regular expressions. Nothing special, on to day 4.
Day 4
For day 4 I solved the first part with an over engineered path finding solution, which turned out to be quite the overkill, but extremely fun to program.
I really like the pattern in use, below is some of the code of it. First you create a Deque
that holds the work, then you load it up with the initial starting points. In the case of the puzzle these are the location of the X
characters.
From there you just loop over the work
, taking a partial solution and seeing if any cells around it will lead to another partial solution, so from XM
to XMA
and on the next iteration to XMAS
. The dx
is a collection of Coord
that indicate valid movements across the board.
A more straightforward approach, which was actually needed for part 2, is to just try to solve it in one step. First you iterate over both y
and x
coordinates looking for an X
, just as above. When you find one, iterate over [-1, 0, 1]
on both the x
and y
axis-es, using dy
and dx
for the direction. If both direction
are 0
, we continue as it would give the current position. The beauty of this approach is that you can move outward in steps, x + 3 * dx
will give you a value 3 cells in the give direction
. From there it is a simple matter of checking if we are in bounds and if the letters spell MAS.
For part 2 a similar approach can be used, however the order is not important. So I chose to create a List
and then check against a target list with the containsAll
method, it does not care about order.
Another reminder to not over engineer at the start.
Day 5
Another fun puzzle, when I initially read it my mind jumped to graphs. There is a 2 part input, the first part being a list of rules, numbers that are only valid when they are placed in front of other numbers.
The second part of the input is a list of report
structures. The first quest was to validate the reports and find only the valid ones.
My first attempt, in part 1, was to take the rules for a number (a List<Integer>
) and see if there is an anyMatch
of the sublist before it using order::contains
. Basically if the pages is 75,97,47,61,53
and the rule 97|75
(97 should be before 75), the the loop will iterate over the pages, and check to see if [75]
is in the list of rules for 97
.
Part 2 had us fixing the broken pages. After some initial magic with arrays I figured out it is a basic sorting problem. In Java you can use Comparator
implementations to create custom sorting rules, as long as it responds with -1,0,1
for to the left, the same, to the right. So the lambda Comparator
takes a left hand side and right hand side value, retrieves the rules for the left hand side (if null
it is equal 0
) and checks to see if the right hand side is in the ruleset (-1
). If all checks fail, the value should go to the right hand side.
A surprisingly easy solution to a messy problem when you want to implement it yourself.
Day 6
Traditionally the Friday puzzles seem to be somewhat more challenging, this Friday is no exception. We are given a challenge similar to sliding puzzle games.
Instead of sliding over ice we are to map the movements of a guard to ensure we can move safely through the area. For part 1 there was nothing too exciting, just move the guard over the floor and track the places visited. Depending on your loop you might accidentally avoid an edgecase that will show up in part 2.
Lets take a look at the loop:
While we are in bounds we keep moving, adding each step into the visited
list. We then get the next position by retrieving the delta (a lookup table of coordinates such as -1,0
, which indicate that the guard will move -1
on the x-axis and 0
on the y-axis). If we are out of bounds, flip the switch and break out of the while
loop, if the next
position is an obstacle, #
, we set the sign
to the 90 degree turned version (another lookup table) and rerun the loop. If, for some reason, you continue checking and validating at this point you might miss the edge-case that turning can result in facing another wall. When all the conditions are checked, simply reset the start
variable to the next coordinates and move on.
Part 2 becomes much more interesting; we are to find infinite loops by placing exactly 1 extra obstacle. Intuitively you will remark that the obstacle can only be placed on one of the cells that were visited in part 1. This already eliminates part of the board. From here you can loop over the list of coordinates, place an obstacle and let the guard run its route. When you visit a coordinate twice in the same direction you know you are in a loop.
I looked for a “smart” solution, but the brute force is done in less then 2 seconds. So I will leave it at this, but somehow feel there might be more optimizations possible.
Day 7
The end of week 1, and easier then the Friday puzzle. We are given a list of numbers per line that we need to either add or multiply to get to a target number. I chose to use some recursion to solve this problem. Each iteration of the recursion will reduce the array of numbers using one of the operations.
In the end the list of numbers will be reduced to either the target number, or something else. So the base case checks to see if it was successful.
If the base case is not hit, the first recursion is to add the numbers. A trick here is to use a LongStream
to range over 1
to the end, mapping the numbers. If number 1
is mapped, we add the number at position 0
to reduce the array.
The second case applies the multiplication in the same way.
The third case (part 2) is to concatenate the numbers, this is easil done through number + "" + number
in java, coercing the numbers into a String
and then using Long.valueOf()
to read the value again.
One last trick is to use the Stream
feature to filter
the list, mapping each object to a long value and summing.
Day 8
Day 8 has us back in history staring at antennas. The description was quite cryptic, but reading it carefully you learn that the necessary step is to find the difference between a pair of coordinates and then extrapolate the path inside the bounds of the grid.
To read the grid into a structure I used a simple nested loop, adding a new list to the map if it is absent, then adding the new coordinate for the antenna.
We then have to find the antinode
for the point which, for the pair, is just a single difference step from the antenna. Interestingly we need to count the unique antinodes. Whenever you get such a requirement, always think about using a Set
for storage.
Getting the pairs is straightforward, and we have done it earlier in the series already. The first loop starts at 0
and ends the element before the end, size - 1
. The inner loop starts at current pos + 1
and ends at the size of the list.
Then just compute the difference and add the antinode when it is in bounds.
In part 2 the path needs to be extrapolated until it goes out of bounds. This can easily be wrapped in its own method.
A pretty straightforward problem to solve, on to tomorrow!
Day 9
For me this was a hard day. We are given a list of numbers that we use to fragment files on a disk. The first part of the puzzle was quite straightforward, create a list that holds the file ids and spaces and just follow the rules.
From there just create 2 pointers, one on the left and one on the right. The left tracks the empty space and the right tracks the file ids that we want to put in the empty space. The important thing in Java is to make the implementation of the list a LinkedList
. This allows for little-overhead reshuffling of the list.
Part 2 became much harder, we are not to find space for the blocks of files instead of fragments. I first tried the same approach, but it took forever. I then saw the error of my ways and decided to use a lookup table for the empty spaces. This table maps the empty spaces of size N
, in the below example 1 and 2, to a list of start/end coordinates.
The list of coordinates needs to remain sorted, so I used a PriorityQueue
for it. Then it is just a matter of determining the size of the file under the r
pointer by looping over it until we hit another id, and then looking up the most left candidate of the empty spaces.
The final solution runs in a matter of milliseconds, so I am quite happy with that.
Day 10
Finally a depth first / breath first path seeker! We need to identify a trail that leads to a summit, or rather all trails that lead to the summit. Part 1 wants to know the score (how many summits can a path reach) and part 2 its rating (how many trails are there that reach a summit). This is pretty straightforward in the sense that you create a Queue
and put the start of the trail in, then for each direction you construct a more complete path.
As always it is important to check for bounds and if the path is incremental (business rule). If the new path is actually at the summit, add it to the finished paths. If it is not, try to complete it.
Day 11 - one to remember
For the last couple of days the discussion forums have been full with memes about brute forcing the answer. Up to now you could really do so. I have one colleague who wrote a nice brute force for Day 6 that took several minutes, but it did work. Personally I am not a fan of the brute forcing approach, I like to make it more elegant when possible.
Today is this years first Lanternfish type of problem, one where the problem space becomes so large that your computer is not able to brute force it due to memory constraints. It calls for a more elegant solution.
Part 1 and part 2 are basically the same, the difference is the amount of iterations for the problem. In this case we have some rules in which rocks change and split up. We are tasked to find the amount of rocks after 25
and 75
iterations. The first is do-able with a brute force approach, the second is not.
The rules are straightforward, but the solution to the problem space might not be. The trick is to use something called memoization
.
In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls to pure functions and returning the cached result when the same inputs occur again.
So, basically we store results of method calls. Lets first look at my solution. It is a recursive function that takes a number
and the number of iterations
to apply to it.
For this call the result is stored in a memo
, a simple HashMap
that stores the method arguments in a CacheKey
and then stores the count, the result of the recursive call. What happens here is that there might be many different calls to this method, such as (9,23)
. In this case the number 9 has 23 iterations to go. By computing the result once and then storing the computed value we save the time of doing the same calculations many other times.
This was a really fun and relatively quick challenge, greatly enjoyed it!
Day 12 - Garden Groups
This day had me stumped for quite a time. The puzzle starts off with a simple question; group the garden (grid) into areas that are the same and count the fences required. A simple flood fill type of solution works very well here.
Whenever a neighbor is not the same type (or we are the edge), a fence is required. If it is the same type, add it to the queue for further processing.
The next part had me going for a little bit. Instead of the area or fences the elves need the sides counted. This turns out to be quite a thing until it becomes clear that counting corners also works.
Lets say the below map is our grid. When looking at the A
in cell 0,2
it is possible to check if it is a corner by checking that the cell above it and the cell to the right are not the same. The same goes for the cell below and the cell to the right.
A neat trick to find the sides to an area. The code turned out to be relatively easy:
On to Friday the 13th!
Day 13 - Claw Contraption
Today was quite something. We have claw machines that have 2 buttons. The buttons move the arm a set space on the x
and y
coordinates. Both buttons have a different value for the cost and we are tasked to find the cheapest path to a prize on some distant x
and y
location.
My initial solution was naive and used dynamic programming to solve it. It did not adhere to the rule every problem has a solution that completes in at most 15 seconds on ten-year-old hardware. So eventually I found a solution (thanks Hyperneutrino!) that uses math to solve this problem.
Basically we are trying to find the amount of x
and y
movements both the A button (indicated by S
) and the B button (indicated by T
) have to make in order to get to the prize x
and y
values.
We can make these equations the same by multiplying with by
and bx
. We do this so we can remove the B button from the equation and solve A.
This can then be rewritten in a single equation, from which we can isolate A.
Now we can divide by axby - aybx
and get our solution for A. We have to ensure the input is never 0
to prevent division by zero. In the code we can check this by checking that ax * by == ay * bx
never occurs.
As we now have the A button value, we can also solve the B button.
In code the solution looks very simple. Notice the ca%1==0 && cb%1==0
check to ensure we do not allow for fractional steps.
Day 14 - Restroom redoubt
Today we are back at Easter Bunny HQ, looking for a restroom. We are given a collection of robots, their current position on a grid and their velocity. The question becomes, where are they after 100 iterations (seconds)? An interesting part of this question is that the robots wrap around the grid.
This mechanic allows for very easy calculation of the final coordinates, as (x + vx * 100) % width
will give us the final position, instead of having to go through all the calculations.
Interestingly, Java does not really like negative numbers in the modulo operator. For example, -102 % 11
yields -3
while it should yield 8
for it to be useful in our case. So, when the number is negative, just add the width to it.
Part 2 was a horrible puzzle. There was no clue what to do in order to get this:
I finally solved it by looking at the field when all robots are on a unique position. I also have seen solution where the minimum safety value is found. The problem description was:
During the bathroom break, someone notices that these robots seem awfully similar to ones built and used at the North Pole. If they’re the same type of robots, they should have a hard-coded Easter egg: very rarely, most of the robots should arrange themselves into a picture of a Christmas tree.
What is the fewest number of seconds that must elapse for the robots to display the Easter egg?
Maybe if it had said “very rarely, but when all the robots arrange themselves”, but then again, how are you supposed to know that it means non-overlapping.
Love the Christmas tree though.
Day 15 - Warehouse Woes
Yay, the Lanternfish have made an appearance! Sadly this puzzle had me quite stumped for a while. I had to rewrite my solution 2 times in order to get it right.
First, let me explain. We are still not finding the historian (whom I think is just Eric in a costume). Instead we are on a side-quest helping our fishy friends with robots in their warehouses. The first puzzle is straightforward; move the player around, moving objects that you run into.
When we have that sorted we are sent to a second warehouse. This time the boxes that we move are twice as large, but the robot is still the same size. This means we get into situations as the following example:
Here the player can move up, but we are only hitting one side of the box. We have to take into account that we need to move the other part along as well. Even more complicated, we can get into the following situation.
In this situation we can not move, even though the 2nd box on the middle layer might think we can, as it has a white-space above it.
I worked on arrays for a while, but eventually went for a more “Java” solution and create the factory as objects. Using the objects it is possible to attack the problem more in a “game engine” type of way, by making each object react to the interaction.
My code is horrible not-optimized, I apologize for that right away, but it gets the job done :D
Firstly, I split the process out into two segments, first to see if we can move, then to actually move. Side note: I should really use a lookup table for the coordinate to find the objects instead of looping over it.
The objects are simple POJOs, all extending the aptly named Thing
.
Thing
has all the logic, with canMove()
and move
basically doing the same thing, except for move
actually moving the objects into a new coordinate. Only if we have a space as neighbor, or if all of the neighbors can move, only then do we move the current object.
Collision is checked against both the left and right hand side of the object. Meaning that we can easily handle boxes of size 2.
Much more work then I thought it would be, but a nice solution anyways.
More to come
[This article will be update with more days]