This year I am going to try a couple of new things. First, I am going to try to use the delve go debugger rather than put in print statements while I debug. My hope is that by the end of December I will be much more familiar with the debugger. Second, I want to try asking some generative AIs for helpful functions to see how it improves my speed. I was considering Github copilot, but I just can't give up my current editor, helix, to use vscode, and I don't really want to go down that neovim plugin rabbit hole anymore.
I may use codespaces a bit. I've added some permissions so that I can clone my private inputs submodule just in case, but it's hard to beat the setup I use to write code every day.
This year I also created a new Advent of Code template in the devries/aoc_template repository. It compiles everything into a single executable and times how long it takes to run each problem, as well as generates a template for each day. It was also an opportunity to experiment a bit with code generation.
Although at times I have not found it, there should be a "solution that completes in at most 15 seconds on ten-year-old hardware."
I am hoping this year to find solutions that will run in a reasonable amount of time on a Raspberry Pi 4 Model B with 4 Gigabytes of RAM and a Cortex A72 processor. The Pi-4 is roughly 500-600 times faster than the first computer I purchased in the late 90s (200 MHz Pentium MMX with 64 Megabytes of RAM). Below I have the time it takes for the problem to finish on the third run of my solution after compilation on my Raspberry Pi.
Day | Part | Time Elapsed |
---|---|---|
1 | 1 | 558.698µs |
1 | 2 | 3.230152ms |
2 | 1 | 1.847944ms |
2 | 2 | 1.950147ms |
3 | 1 | 3.929274ms |
3 | 2 | 4.738173ms |
4 | 1 | 4.211679ms |
4 | 2 | 3.712758ms |
5 | 1 | 3.235819ms |
5 | 2 | 1.154803ms |
6 | 1 | 31.777µs |
6 | 2 | 30.13µs |
7 | 1 | 4.545508ms |
7 | 2 | 5.002041ms |
8 | 1 | 3.015281ms |
8 | 2 | 23.987541ms |
9 | 1 | 2.51753ms |
9 | 2 | 2.292069ms |
10 | 1 | 59.225004ms |
10 | 2 | 119.835531ms |
11 | 1 | 919.568362ms |
11 | 2 | 908.9021ms |
12 | 1 | 7.49596ms |
12 | 2 | 129.23797ms |
13 | 1 | 708.271µs |
13 | 2 | 711.826µs |
14 | 1 | 3.185616ms |
14 | 2 | 1.734193401s |
15 | 1 | 460.069µs |
15 | 2 | 3.105932ms |
16 | 1 | 47.589642ms |
16 | 2 | 9.03134853s |
17 | 1 | 4.215648433s |
17 | 2 | 13.861613972s |
17 | 2b | 1.055163261s |
18 | 1 | 294.590182ms |
18 | 2 | 3.934349ms |
19 | 1 | 3.561334ms |
19 | 2 | 4.006922ms |
20 | 1 | 25.246853ms |
20 | 2 | 2.752046ms |
-
Day 1: Trebuchet?! - ⭐ part 1, ⭐ part 2
Initially for the second part I put together a regular expression to search for the spelled out digits, however it is possible to have overlap between these words, for example "twone", "sevenine", or "threeight." Regular expressions do not capture overlapping matches, so I switched to cycling through the names and finding the indecies of each word or digit (if any) using
strings.Index
. Once you find a match you have to look for more matches starting at the next character after your previous search and I did have some indexing issues which I had to debug. I later realized I could usestrings.LastIndex
to make this much easier. -
Day 2: Cube Conundrum - ⭐ part 1, ⭐ part 2
Most of this problem was parsing. I just did a lot of splitting on substrings. First I split on ": " to separate the game id from the draws, then I split on "; " to separate the individual draws, then on ", " to split to the colors. After that it was straightforward. I missed an opportunity to use the debugger to check my parsing and instead put in a
Println
. -
Day 3: Gear Ratios - ⭐ part 1, ⭐ part 2
The key here was just figuring out how to store the schematic data so it would be useful for answering the questions. During parsing I recorded the positions of the symbols in a map indexed by position. I recorded the numbers as an array of structs including the value and the start and end position. Then I could interate over all the numbers, find the surrounding points, and see what symbols were around them. I created an array of numbers for each gear object I found and then could iterate through those arrays to find gears with exactly two adjacent numbers.
-
Day 4: Scratchcards - ⭐ part 1, ⭐ part 2
The challenge for today was competing without any power. Eventually I set up a hotspot on my phone to connect via a very slow (1 bar LTE) connection and read the problem as well as download the input. I am so glad that the Advent of Code site is all text. As for the problem? I am cold and tired so I am not sure it was my best code, but I just iterated through the cards while creating an array of the number of copies of the card which I called
multiplier
. Because go arrays default to 0, I decided it would be easier if the multiplier array just had the count minus 1 so that a multiplier of 0 would mean 1 copy. Everything seems to have worked out in the end, though I still don't have power, and it's not getting any warmer. -
Day 5: If You Give A Seed A Fertilizer - ⭐ part 1, ⭐ part 2
This is the sort of problem where it is prohibitively large to calculate the conversion of every individual element in a range of integers, however the given ranges of integers need to be handled in different ways depending on where they exist within portions of those ranges. Rather then iterate through each range, the key is finding the subranges which are handled in the same way and calculate how that range as a whole will be modified. As you continue to do this the number of ranges grows, but it will always be far fewer calculations than tracking how each individual element is handled.
-
Day 6: Wait For It - ⭐ part 1, ⭐ part 2
This was just solving the quadratic equation for the times when the distance was equal to the time of the race. There was a bit of fiddling with checking if the distance was greater than or equal to the winning distance, but all in all that was the gist of it.
-
Day 7: Camel Cards - ⭐ part 1, ⭐ part 2
I thought this was an interesting one. Initially it involved parsing the card values and then scoring based on the count of cards in the hand and the values of the cards, but in the second part adding a wildcard put in a twist. I added the joker counts to the card with the highest count, so for example if I had two 3s, one J, and two other cards, I would add the J to the 3s making three of a kind. I got a bit hung up on the golang sorting pattern, maybe because I hadn't had my coffee?
-
Day 8: Haunted Wasteland - ⭐ part 1, ⭐ part 2
This is a series where you calculate the least common multiple of a set of cycling states in order to find the interval over which all of the states fully run through their cycles. I initially printed out the step at which the cycle started and how many steps until it repeated. I noticed that the problem is contrived such that the number of steps it initially took to reach the desired end state was equal to the interval between times it hit that end state, which makes the problem a straighforward least common multiples problem. If the problem had not been contrived in that way, it is possible that there would not have been a period over which all starting states eventually synchronize so all ending states are reached at the same time.
-
Day 9: Mirage Maintenance - ⭐ part 1, ⭐ part 2
I thought about trying to somehow be clever and calculate only as much as I had to at the edges of the sequences, but then I thought each sequence is short so I just followed the procedure in the example. It turned out to be very straightforward.
-
Day 10: Pipe Maze - ⭐ part 1, ⭐ part 2
The first part was streightforward, follow the pipe in both directions until your search meets at the farthest point from the start. The second part took some time for me to think about. Eventually I settled on counting pipe intersections on a walk directly to the north from the point of interest. A point on the outside would intersect the pipe an even number of times, while a point on the outside would intersect an odd number of times.
-
Day 11: Cosmic Expansion - ⭐ part 1, ⭐ part 2
Straightforward again. I used the github.com/devries/combs library to get all the pairs of galaxies and then summed over the columns and rows that separate them, multiplying by the expansion factor if the column or row did not contain a galaxy.
-
Day 12: Hot Springs - ⭐ part 1, ⭐ part 2
This one was very tough for me. I spent a lot of time making iterators that would return the next potential valid row with one additional group filled in, but I wasn't able to memoize that method and I did not take into account the idea that there was a maximum number of working spring spaces I could put in before adding the next group. I ended up parameterizing solution counts in a memoizable state which, for each sequence, was the number of groups already accounted for and the starting position in the sequence. I would then run through all possible positions of the next group up to the total amount of buffer space I had available and find counts for those. I find these kinds of problems very difficult.
-
Day 13: Point of Incidence - ⭐ part 1, ⭐ part 2
I noticed all the maps were smaller than 64 in length and width, so I used a bitfield to store the locations of the rocks for every row and column. I then just had to check for symmetry from each row and column gap. For the second part I did the same comparison but checked to see if there was an off by 1 bit issue between any two comparisons, and required one and only one of those to define a new symmetry axis. Unfortunately I tried to use subtraction rather than the XOR function, which of course caused a few errors. My extra test cases were made to track down those errors. I used Kernighan's bit counting algorithm to find the number of bits in the XOR difference.
-
Day 14: Parabolic Reflector Dish - ⭐ part 1, ⭐ part 2
The tilting mechanic was fairly straighforward, though for the spin cycle I decided to write four separate loops rather than a more generic loop which would work for each direction. Obviously iterating 1,000,000,000 times is not practical, however the arrangement of rocks should begin cycling through a sequence of repeated positions, so we just need to find where that cycle starts and ends, and then jump ahead N cycles to just before the iterations are complete. The only problem is storing the map state. This tripped me up a bit as I tried to find a unique integer that summarized the map, but that's the job of a hash function. I took the sorted list of positions of rolling rocks, turned the coordinate into an integer, sorted them, and then wrote the binary encoding of those integers to a fnv-1a hash function. I then was able to look for when the hashes started to repeat. This is the first day that takes more than 1 second to run on the Raspberry Pi (though day 11 came close).
So far I have not used any AI generated subroutines in my code. Today may have been a good day to try. I could have asked something like "write a function in go that calculates the hash of a slice of int32s." Having just asked, bing generated the following code:
import ( "crypto/sha256" "encoding/binary" ) func HashInt32Slice(s []int32) [32]byte { h := sha256.New() for _, i := range s { b := make([]byte, 4) binary.LittleEndian.PutUint32(b, uint32(i)) h.Write(b) } return h.Sum(nil) }
While programming it seems like I don't really want to shift gears to ask AI for help.
-
Day 15: Lens Library - ⭐ part 1, ⭐ part 2
This was also a very straightforward problem, although I did have to fix a number of bugs I introduced. This problem illustrates how a hash map works and then requires operations on the hashmapped values. I created a Lens object which I passed around by value, and forgot when I changed an attribute in that object, I need to reassign it to the lens in the box array.
-
Day 16: The Floor Will Be Lava - ⭐ part 1, ⭐ part 2
My breadth-first search library came in handy for exploring the contraption. I used the beam position and direction as the state. I also made use of the new go
clear
built-in function to clear out the energized tile hashmap. This one is a bit slow, but I guess it is fast enough? I may have been able to save the number energized starting from each state in the system to reuse when I ran additional starting directions, but that seems like it would have been tricky with the requirement that energized tiles only be counted once. -
Day 17: Clumsy Crucible - ⭐ part 1, ⭐ part 2, part 2b
First use of Dijkstra! It was fairly easy to adapt the conditions of the problem to the Graph routines in my code. I think memory allocation may be the bottleneck in my implementation, but it seems fast enough.
After submitting my solution and reading other people's suggestions on reddit I refactored my solution for part 2 to part 2b. In this example, rather than use the position, direction, and number of steps as a state, I reduce the state to position and direction. For neighboring states, I assume I will turn 90 degrees and find all states 4 to 10 steps away in each direction. This runs much faster as shown above.
-
Day 18: Lavaduct Lagoon - ⭐ part 1, ⭐ part 2
For the first part I did a flood fill of points outside the trench and then subtracted the area not filled from the surrounding rectangle. I should have seen the second part coming, but essentially what I did was find all the horizontal line segments in the trenches. I then worked up from the lowest segment to the highest segment calculating the area of rectangles above the existing segments until they intersect other segments. I did the accounting in a complicated and error prone way which meant that I was able to only finish the first part before work and had to wait to finish the second part until after work. I think there has to be a more elegant way to express what I was trying to express, but I didn't find it today.
-
Day 19: Aplenty - ⭐ part 1, ⭐ part 2
Today's code was heavy with structures. One for the part, the part range, the operation, the workflow, and the rule. It got a little complicated, but it was just breaking things down and then evaluating the next step. For the final summation, I would just split the available ranges up and perform the rule action on the part that satisfied the rule, and move on with the part that didn't.
-
Day 20: Pulse Propagation - ⭐ part 1, ⭐ part 2
The first part of this was straighforward, though I did manage to overengineer it a bit. I also did finally ask for bing chat to provide some code. I asked it for an arbitrary length bitfield. For some reason it didn't give me an unset method and I had to write a serializer and deserializer because I thought we were going to require memoization. This is one of the many times thinking ahead just made things harder because it turns out the second part involved examining the structure of the gates to form a solution. I only did the solution for my particular arrangement. The code to part two does produce a visualization which can be used to change the final 4 binary entries in the program.
-
Day 21: Step Counter - ⭐ part 1, part 2
I did part 1 of this probem and then put aside part 2 for later in the day. I was working on adding up all the maps in a diamond with alternating odd square and even square map counts, but I had some arithmatic errors and offset issues. Eventually I decided to put it aside. Some other issues started taking up my time including a large refactoring project at work and my children arriving for Christmas, so I didn't get back to it this year. I will probably finish things up in the new year.