Advent of Code 2024 in 24 + 1 programming languages
There’s a yearly series of code challenges called Advent of Code. I participated in the first one in 2015 but didn’t do any since then – at least, until this year. I decided to give this year’s one a try, but with a twist: I would use a different programming language for each day’s challenge.
The rules:
- Each day’s program must take input from a file and output the answer to
stdout
. If a programming language is incapable of reading files, then the program may read fromstdin
(piped from a file) – this was necessary only for the Setanta solution. Also, the Day 14 Part 2 solution prints additional output because of the nature of the problem. - No external dependencies are allowed. Exception: Gleam doesn’t have file-reading functions in the standard library, so it uses
simplifile
. - Parts 1 and 2 are separate programs.
- I avoided programming languages that are overly similar, or where one is almost a subset of the other.
I split the challenge into three legs:
- The dynamically typed languages (Days 1 – 10)
- The statically typed languages (Days 11 – 21)
- The really fast statically typed languages (Days 22 – 25)
Day 1 (Historian Hysteria) – Raku
Difficulty: Easy
Starting off with an easy challenge in one of my favorite languages! Part 1 was a one-liner, while Part 2 was a four-liner. Look, I can even paste it here:
# Part 1
say [+] map &abs, [Z-] map &sort, [Z,] "01/input".IO.lines.map(*.words);
# Part 2
my $ls = [Z,] "01/input".IO.lines.map(*.words);
my @l = $ls[0];
my $r = bag $ls[1];
say @l.map({$_*$r{$_}}).sum;
The only reason it took 4 hours is because I forgot about Advent of Code until almost 4 hours into the first day.
Day 2 (Red-Nosed Reports) – Ruby
Difficulty: Easy
Another easy challenge, but in a language I’m unfamiliar with.
Day 3 (Mull It Over) – Racket
Difficulty: Easy
Took some effort to find out how to use regexps in Racket, but overall, not a hard problem.
Day 4 (Ceres Search) – Setanta
Difficulty: Easy
Again, this is conceptually easy, though I ran into quite a few off-by-one errors with this one.
Day 5 (Print Queue) – Lua
Difficulty: Easy
Part 1 is a matter of checking that no pair of pages has a rule with the pages in the opposite order. You might think that Part 2 needs a topological sort, but I managed to do it with the built-in table.sort
function: if there’s a rule prescribing an order for two pages, then return that for the ordering; otherwise, return the order they were in. The rest is dealing with Lua itself: it doesn’t have a string-splitting function, so I had to write one myself. Why does anyone use this language?
Day 6 (Guard Gallivant) – JavaScript
Difficulty: Hard
I personally had trouble with Part 2: my solution would give the correct answer for the example input but not my actual one. I frantically tried to debug my program until I realized that it didn’t properly handle the case when the guard turned twice in a row. So this:
if ((map[row2][col2] == '#' || (row2 == obrow && col2 == obcol))) {
let dx2 = -dy;
dy = dx;
dx = dx2;
}
turned into this:
if ((map[row2][col2] == '#' || (row2 == obrow && col2 == obcol))) {
let dx2 = -dy;
dy = dx;
dx = dx2;
continue;
}
My Part 2 solution still took 38 seconds, but that was good enough to solve the problem.
Day 7 (Bridge Repair) – Common Lisp
Difficulty: Easy
Another unfamiliar language today (hi, Iso!). The parsing code took me a while to figure out, especially as I was reading this page to find out how to use loop
. At first, I thought that I would have to write all the string-handling functions myself, before I realized that they were there, but as sequence functions. (One of the pages on The Common Lisp Cookbook led me in the right direction.) Common Lisp also lacks a built-in string-splitting function, but for my use case, I can wrap the string in parentheses and read it as a list.
Aside from that, the problem itself was an easy recursion.
Day 8 (Resonant Collinearity) – Uiua
Difficulty: Medium
Part 1’s solution involved finding all pairs of antennae with the same frequency, getting the antinodes, and removing duplicate elements. Part 2 was largely the same, except that I calculated the bounds for the indices for which was in bounds. This took a lot of finagling with the stack but produced the correct answer in the end.
I thought that my Part 2 solution could get some improvement to be more idiomatic. My new solution started from and stepped by until the coordinates were out of bounds. The revised solution is in 08/08b_v1.ua
; the v
s are zero-indexed, so any _v1
file is newer than the corresponding file without _v1
.
(Technically, I handled the case when the offset between the antennae was reducible, but this might not have been necessary?)
Day 9 (Disk Fragmenter) – Python
Difficulty: Medium
Part 1 was easy (modulo edge cases); part 2 required a lot of thinking to find a reasonably efficient approach. I initially tried explicitly representing gaps, but that would have required logic for merging them. In the end, I only represented the files explicitly, with the gaps being implicit.
Still, my initial part 2 solution took 15 seconds, so six days later, I revisited the problem. I noticed that merging gaps was unnecessary since the space a file is moved from is never repurposed, so it sufficed to keep a list of gaps for each length (from 1 to 9). My new solution finishes in under 100 milliseconds.
Day 10 (Hoof It) – Perl
Difficulty: Easy
We finish our first leg with Raku’s ancestor and the language that Raku was originally named after: Perl!
Initially, I misread Part 1 and calculated the number of distinct hiking trails from each trailhead. After I fixed my code, I noticed that this was what Part 2 called for! Less than two minutes later, I had a solution for that.
Day 11 (Plutonian Pebbles) – Java
Difficulty: Easy
Part 1 was straightforward. Since Part 2 replaces the number 25 with 75, it should be easy as well, right? Turns out that even with an optimized LongList
implementation, my code couldn’t get past 42. I initially thought that the list of stones might have some kind of redundancy in them, but the key insight is a lot simpler: since the order doesn’t matter, you can store the stones as a HashMap<Long, Long>
.
Day 12 (Garden Groups) – Haskell
Difficulty: Medium
Ordinarily, I would use a mutable array to represent the regions during flood filling, but we can’t have that in Haskell (at least, not without a lot of trouble), so I used sets instead. (I could still use an immutable array for the garden itself, though.) Part 2 checks whether each fence is the leftmost (or rightmost, depending on the orientation) fence of the side. The rest is a matter of relearning Haskell.
Day 13 (Claw Contraption) – Nim
Difficulty: Medium
Part 2 is a matter of solving systems of linear equations (nothing is degenerate). For some reason, I solved them in a roundabout way using an extended Euclidean algorithm, but this was unnecessary.
Day 14 (Restroom Redoubt) – Kotlin
Difficulty: Hard
Part 2 required finding the first time when a group of robots form picture of a “Christmas tree”. When I read the statement, I panicked and looked through the Advent of Code thread on a Discord server. Some people managed to find the answer by finding the first time that all the robots had unique positions, but as my frame was late in the sequence, this didn’t work for me. Eventually, I decided to print the first 1,000 frames out for inspection, then 3,000, then eventually all WIDTH * HEIGHT
frames. Since this took too long to inspect by hand, I searched for a row with enough @
signs and found the right frame (no pun intended).
While I initially disliked this problem, I’ve found it fun in retrospect.
Day 15 (Warehouse Woes) – Gleam
Difficulty: Medium
Another problem made harder by working in a purely functional language, in my opinion.
Day 16 (Reindeer Maze) – Scala
Difficulty: Medium
Part 1 is a straightforward implementation of Djikstra’s algorithm; Part 2 runs the search to completion while storing the predecessors to each node.
Day 17 (Chronospatial Computer) – Go
Difficulty: Extreme
Part 2 can feasibly be brute-forced with enough attention to performance and use of multithreading, but I didn’t think that was a viable option for Go, especially since I was unfamiliar with the language. I didn’t do much to inspect the structure of the program I was given, so I turned to expressing the problem as a Boolean satisfiability problem and writing a simple SAT solver to solve it. This was extremely error-prone, so I took a few measures to ease debugging:
- Checking my SAT input against an existing solver (Varisat, in my case)
- Storing comments to explain each operation and block of bits
- Having a messed-up sleep schedule
In the end, this took almost six hours to complete.
Day 18 (RAM Run) – C#
Difficulty: Easy
An easier maze-based problem; for Part 2, a straightforward linear search works well enough.
Day 19 (Linen Layout) – F#
Difficulty: Easy
For Part 1, a recursive solution performs well enough. This poses a problem for Part 2, though, so I slapped some memoization on it to make it run faster.
Day 20 (Race Condition) – OCaml
Difficulty: Hard
Initially, I tried storing the cheat state of each node (either not having cheated yet, currently using a cheat, or having cheated). After a lot of debugging to weed out edge cases, this produced the correct answer for the example input but took too long for the real input. After unsuccessfully trying a few other approaches, I had the insight of running BFS twice: first to find the time needed to reach the end from each position and second to find each route in a real run. In particular, the latter step involves jumping from each visited node to a node 2 blocks away and finding . Part 2 is then straightforward, instead iterating through a neighborhood with a radius of 20 (and using the L₁ distance from the current node to the new one instead of 2).
Day 21 (Keypad Conundrum) – Swift
Difficulty: Hard
I initially tried a top-down approach for Part 1, but it’s not clear which order of movements is optimal for moving through the numeric keypad. Instead, I worked from the bottom up:
- Find the lengths of the optimal keypress sequences moving between each pair of keys for the third robot.
- Then use that to find the same for the second robot.
- Then use that to find the same for the first robot.
The advantage of storing only the lengths is that you get Part 2 for (almost) free.
Day 22 (Monkey Market) – D
Difficulty: Easy
Part 1 is strightforward programming. For Part 2, scan each monkey’s sequence and tabulate the price for each sequence of four changes (stored as a uint
in my code). The result for each monkey can be added together, and the maximum value of the result is the answer.
Day 23 (LAN Party) – C++
Difficulty: Medium
Part 1 involves finding 3-cliques (with some logic to avoid duplicates in my solution). For Part 2, I used a dynamic programming-ish solution, starting with 3-cliques and then growing each clique by 1 until no cliques remain.
Day 24 (Crossed Wires) – Zig
Difficulty: Extreme
Part 1 is easy enough. I wasn’t feeling like implementing another SAT solver for Part 2, so I decided to search for the carry wires for each place. Since for each place, we only have to worry about three inputs, we can store the behavior of each output with 8 bits. Unfortunately, trying all permutations of 8 distinct wires will take too long. Instead, we perform a backtracking search: if we can’t find the carry wire or the output wire doesn’t have the expected behavior, then try all swaps of two wires (if you haven’t already swapped four pairs of wires). (I didn’t happen to have an input that requires multiple wire swaps within a place.)
I found writing Zig code painful – the compiler doesn’t typecheck a function unless it’s called from main
. At least C++ does that for functions that don’t use templates. Also, needing parentheses after an if
or while
was annoying after years of writing Rust. Speaking of Rust, I’ve saved it for the last day. With the fiendish Day 24 challenge out of the way, there’s just one more problem to go.
Day 25 (Code Chronicle) – Rust
Difficulty: Easy
Trying all lock–key combinations is fast enough here. Part 2, of course, is trivial.