sharick.xyz Home Blog Projects Directory Misc Contact

Advent of Code: Reflections of a Marginal Leaderboarder

Tags: Advent of Code
A screenshot of the Advent of Code global leaderboard, showing David Sharick at 80th place with 759 points

My placement on the final AoC 2023 global leaderboard

Advent of Code

Advent of Code (AoC) is a yearly Advent calendar of programming puzzles that's run since 2015, with 25 two-part problems each year, each part rewarding one "star". For each star, the first 100 people to solve the puzzle gain points based on their rank, from 100 for the first-place finisher down to 1 point for the 100th. These points are used to rank users on a global leaderboard, which displays the top 100 participants by global score; the minimum number of points for the global leaderboard varies by year, but is typically in the 600s. 2023 was the third year I participated in AoC, and the first one I was able to place on the global leaderboard; I won 759 points, enough for 80th place. The fact I managed that came as a huge surprise to me: going into December, I expected to maybe place on the global leaderboard a couple of times, getting maybe a couple hundred points total (like I did in 2022, where I placed on two days for a total of 92 points). Instead, I managed to reach the top 100 on no less than eight different days, far more than I would have ever imagined before.

I'd describe myself as an example of a "marginal leaderboarder". As opposed to the "regular leaderboarders", those who regularly place on the top 100 day after day (see the top 20 or so people on the global leaderboard for examples), marginal leaderboarders reach the top 100 only occasionally (for some meaning of "occasionally"), and if they do this often enough make it to the global leaderboard off of those occasional placements. In other words, insofar as this is an advice post, its advice is for those who can reach the global leaderboard sometimes, but can't do it consistently; it's my ideas on how to make those inconsistent victories happen more often.

A screenshot of the Advent of Code personal stats page, which shows states for each day in six columns: time, global leaderboard placement, and global leaderboard score, for both part 1 and part 2. The most successful days have times usually under 10 minutes and scores and placements that are both double digits, while the least successful days have multiple-hour times and placements above three or four thousand, with scores of 0.

My placement for each AoC problem; note the heavy variation between as high as top 20 for some problems and as low as the 4000s for others

But first, some disclaimers:

TL;DR: take all of this with some healthy grains of salt.

Be on time

It might seem obvious, but the most important thing you can do is to ensure you start each daily puzzle as soon as it opens at midnight Eastern time, to maximize the amount of time you have to solve it while still earning points. Unfortunately, this is quite dependent on your timezone: I benefited from living in the eastern US and working a remote job at the time, meaning I could reasonably be awake at the time and could code for a long time without getting in the way of other obligations; if you have work/school/etc during midnight Eastern, or need to be asleep then to make it to those things, you'll have a lot of issues placing on the AoC leaderboard. If you don't, however, then make sure to block out that time slot: avoid making commitments where you won't be able to be coding when the problems release, and plan to be ready at your computer a few minutes before the puzzle unlocks so you can mentally and physically prepare. If you need to leave fairly soon after the puzzle unlocks, or you need to get to sleep, you can wait until the global leaderboard fills up and then give up if you haven't already solved the problem; the only consequence to leaving problems unfinished is that you lose the ability to complete day 25's part 2, and you can always do them when you have more free time.

Practice

Another obvious-sounding tip is that practicing is vital, and in particular, practicing previous Advent of Code problems is vital. Although there are similarities between AoC and other programming challenges like Leetcode, for improving at AoC you'll want to specifically practice AoC problems to get a better grasp of the specific patterns that are unique to AoC. The fact that AoC only presents a single known input, as opposed to multiple hidden ones, opens up the ability to inspect the input for useful information, as well as a class of problems that actively require reverse engineering the input, such as 2021 day 24. There's also entire solution patterns, most notably cycle detection, that practicing previous years' AoC problems will leave you much better equipped to spot and implement quickly. If there's a specific skill or problem type you want to practice, the categorization and mega-guide available on Reddit is an amazing resource for finding which past problems fit your goals. In my case, between finishing AoC 2022 and starting AoC 2023, I went back and completed all of AoC 2015 through 2020, working on my personal library as I went; without this, I strongly doubt I could've managed to place as high as I did.

Write at least one test

If you're trying to code as quickly as possible, it might seem tempting to just submit the first plausible-looking answer your code spits out, rather than wasting time testing it. This is a trap: although you don't want to spend time writing lots of test cases for every function you write, you should always try to write at least one test based off the sample input given by the problem. If you have a good skeleton for writing solutions, you should be able to write a sample input test in only a few seconds, while every wrong answer you submit will cost at least a minute, and often more. The risk of losing so much time is almost never worth the time you save by avoiding tests, unless you're not someone this post applies to; this is a lesson I've learned the hard way. That being said, don't spend too much time on testing; it's rarely, if ever, worth writing tests that you have to think about, as opposed to those you can get from the sample data given by the problem.

Prepare a solution skeleton

An important part of solving puzzles quickly is to spend as little time on anything other than writing solution-specific code as you can. An important part of this is that you'll want to have a good skeleton to take care of writing boilerplate, importing libraries, and making it easy to run both the solution itself and any tests you write. My skeleton for AoC 2023 covers these basic goals, with all the imports I usually use, skeletons for parse_input, part1, and part2, and a main that runs either my tests or the part 1 and part 2 code. I also have 16 different prewritten bodies for parse_input, which is useful for writing input parsing quickly: I can just delete all the code before my preferred function body, and then either use it as-is or edit it further. There's also a meta level of scaffolding code you'll want to write, to make copies of your skeleton and download each problem's input; in my case, I used the scripts download-input.py and make-folder.sh, and ran the command ./make-folder.sh {day} && ./download-input.py {day} && cd day{day}/ && code day{day}* each day, which reduced the time taken to start coding to under 10 seconds. A final thing you'll want to have if you're using a compiled language is a way to streamline running the code, such as a Makefile, to avoid wasting time on that. Like your personal library, your skeleton will also naturally evolve as you think of new functionality that would be valuable to include.

Write a personal library

Writing a personal library (emphasis on personal) for commonly used algorithms and operations, rather than needing to code them up on the spot or search your previous solutions for them, is a way to save a lot of time whenever those functions are relevant. For example, writing utility functions to parse the input quickly can save multiple minutes for common input structures, like one string per row, a 2D grid of characters, or a set of 2D points, letting you move to problem-specific code quickly. Data structures like graphs come up in a lot of different problems, and having operations like shortest-path search or connected components can trivialize problems that require them; having nearly the entire solution prewritten is the best way to get onto the leaderboard. (For graphs, you can also use a third-party library like Python's networkx if you're willing to use libraries, but there are advantages to rolling your own and there are other types of utilities that don't have as high-quality libraries available.) The fact it's a personal library is key: it can naturally grow as you complete more problems and think of useful functions to include, and the fact that you write it yourself means that you'll hopefully be more familiar with everything it includes and how to use it. My own personal library, which was vital to multiple leaderboard placements or otherwise strong performances (days 9, 11, and 17, among others), is an example of what a personal AoC library might look like after working through most years.

Know your language

Because you can use any language for AoC (even one you write yourself), I won't advise using any particular programming language here. However, that doesn't mean your language choice doesn't matter; rather, you should use whichever language you feel most comfortable in, and make sure you have a strong understanding of its semantics, its obscure utilities, and its important libraries. For example, I do AoC in Python, and knowing about Python's ord() function to convert from characters to ASCII codes without looking it up was essential to solving day 15's part 1 in 1 minute and 50 seconds; it's very possible I wouldn't have placed on the part 1 leaderboard at all without knowing it, since the cutoff for it was only 20 seconds slower than my time. I don't know enough about other languages to recommend similar features and utilities to learn about in them, but I can list some other Python features that have helped me solve problems quickly: the collections library (especially Counter and defaultdict), the itertools library, cache from the functools library, and unpacking tuples/lists. Another aspect to "knowing your language" is that you'll ideally want to practice in that specific language, which ensures that you'll be able to build up a personal library and a code-running infrastructure for it, as well as that you have a reservoir of previous solutions to tap if needed.

Conclusion

I can't think of much else to say at this point (and I'm already a couple months late on finishing this post), so I'll leave with this: I hope this has been interesting to read, and I hope that anyone who wants to improve their skills for the next Advent of Code finds this helpful towards that goal. If that describes you, I'd also recommend getting a head start on practicing now, so you have time to pick your language, build up a library, and get experience by running through previous years' problems.