World Playground Deceit.net

Advent of Code 2024: retrospective


Album cover

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)
NIL
Q3CPMA-USER> (separate (iota 10) #'evenp)
((0 2 4 6 8) (1 3 5 7 9))

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.