Advent of Code 2024: retrospective
As anyone who read the previous post could have guessed, the Advent of Code is the main reason I haven't been blogging this month. And as (roughly) expected, I stopped near the last third.
There are two reasons for this expectation:
- What I seek from the AoC is a programming challenge, where I have fun engineering the solution in a way that pleases me. Once the problem solving part starts taking too much of my time and I need to write CS algorithms named after their inventor(s), I quickly lose interest.
- Work was pretty tiring. Coming back home after 7h of programming and doing another hour (or more for the few days that tripped me) is only acceptable while it's fun.
I especially dislike dead ends like 17p2 that require an Eureka to progress and where smart brute force doesn't cut it anymore.
On the whole §
I had fun, found the difficulty better spread the last year (from memory) and got many ideas
for my CL utility library. Used my #λ
reader macro a lot.
What I'll do now is describe some highlights and cool snippets/ideas I got from specific days.
Day 1
See my initial post
Day 2
Always nice to compute the deltas of a sequence using (mapcar #'- list (cdr list))
Discovered the obscure constantly
that helped me to remove a specific element
from a list like so: (remove-if (constantly t) list :start s :count 1))
.
Day 4
As recommended by some people, using complex numbers for positions and vectors in the numerous
grid problems is a nice idea, since they can be summed or rotated with ease. This is how I made grid.lisp which contains direction constants/functions and grid manipulation utilities (mainly I/O and
wrappers around aref
, array-in-bounds-p
and my array utils).
Said grid stuff also made me add ways of coercing between complex numbers and lists/conses.
Had fun finding a way to reuse part 1 in part 2: find all diagonal MAS, return the As' positions and count the duplicate positions.
Day 5
Immediately recognized a topological sort, knew that POSIX had tsort
which I
simply called in my CL. Added a wrapper around the Lovecraftian uiop:run-program
for the most common use case: (run argv0-or-argv :input line-or-lines) => stdout-lines
Also remembered that CL had a few set functions operating on lists, like subsetp
.
Golfed it in POSIX sh too, for fun.
Day 6
First day that tripped me slightly. Remember: being smart when brute force works perfectly is a recipe for disaster in the AoC.
PS: Symbol macros
and circular lists are rad.
Day 8
Felt incredibly good to map on all pairs of antennas using that (mapcon #λ(mapcan ...))
contraption. Realized later that alexandria:map-combinations
(doesn't collect, weirdly) already exists, though...
Added a pretty cool sequence utility classify
(and its special case separate
) to put items in buckets according to a key function:
Q3CPMA-USER> (maphash #λ(format t "~A: ~A~%" _1 _2) (classify (iota 10) #λ(mod _ 4))) 0: (8 4 0) 1: (9 5 1) 2: (6 2) 3: (7 3) Q3CPMA-USER> (separate (iota 10) #'evenp)
Day 9
First day that tripped me hard. Like my part 2 code working for lots of different samples from anons but shitting itself on the real input. Finally triumphed at something like 23h, and thanked God I already had a deque in my toolbox.
Probably should have changed the data structure from part 1 to 2.
Day 11
I usually like those just crank a number for part 2
days. Unlike most people who
used memoization, I went the stone -> count hash table route. Worked nicely and I had the time to
do some performance comparison with an anon who did it in Python.
The result: SBCL was 10 times as fast for small and large (custom) inputs. Another interesting figure is that on the large case, my AMD 5900X took 25 ms vs my work MacBook Pro (M2)'s 33.
Day 12
Second hard day, triggered some flashbacks of last year's pipeworks. To be honest, without the advice I found on the web to count corners instead of sides, it would have been painful.
Added rotate-left
(and right) to do what it says on the tin to lists.
Day 14
Part 1 was a piece of cake and the cunningly unspecified part 2 was great fun! Unlike the 17p2 that stumped me, you could solve this one in many ways (I chose the heuristic of detecting robot clustering via statistics).
Made me think that, like make-pathname
, both make-array
and make-hash-table
should have a :defaults
keywords to copy
parameters and/or contents from another instance.
Day 15
Pleasantly solved part 1 (and thus horizontal part 2) via recursion, but got stuck due to a stupid typo on vertical part 2 that only triggered on a quite specific case, forcing me to watch hundreds of "frames" of a drunk robot pushing boxes. Still fun!
Day 16
Shamelessly plagiarized a lainon on this one, dreadfully boring and too tired.
Gave me the idea of a map-maybe
alias for (delete nil (mapcar ...))
, Tcl spoiled me on the matter (you can map and filter at once with lmap
).
Day 17
The last day for me. While I failed part 2, it allowed my to use CL's unique party piece:
runtime compilation! Sure, any language with potent enough macros could have read the program and
translated it at compile time, but it's just so incredible cool to build your lambda and just compile
it while your program is running.