Advent of Code 2023: What I learned
Advent of Code is a coding competition, with one new challenge per day during December. I started writing the core of my own solutions, then I realized this wouldn’t be very interesting because (1) my own code is often very naive compared to the best ones, (2) the article was starting to get very long.
Instead, I will focus only on things I learned or already knew but experienced doing the challenges. I’ve been doing my implementations in Python and in C, but also read other solutions (mostly posted on Reddit) and have often been amazed by one-liners or very performant implementations.
Python
Logging
Logging is important, especially when you need some form of debugging
|
|
Along with logging, I appreciate the f-strings notations where the variable to output is directly shown in the format string.
Comparison
It’s actually possible to write several comparisons like:
|
|
Type hints
Type hints are very probably over-kill for Advent of Code challenges, but coming from C, I like to use type hints as much as possible.
|
|
- Type hint for an array of ints:
list[int]
- Type hint for an array of arrays:
list[list[int]]
Enumerates
I wasn’t using very much enumerates in Python. They are very useful to simplify loops that need counters.
For example, in the following code, we automatically “map” the words to an incremented index:
|
|
Lists
When you pass a list to a function in Python, it’s the object that you provide. So, if the function modifies the list, the list will remain modified outside the function.
Example:
|
|
If you don’t want that to happen, you can copy the list with mylist.copy()
.
List comprehensions
Reminder: it is possible to use conditions in list comprehensions.
For example, the following get the positions of O
s:
|
|
Tuples
The difference with a list is that tuples are immutable. You cannot assign to a tuple.
Sets
To initialize a set: a = set()
A set does not have redundancies.
So, to test that all elements of a table are zeroes, you can actually transform the list in a set and test: set(tab) == {0}
- Reminder: a set is defined with
{}
. A dict is a named set:{ 'green' : True, 'blue' : False }
.
Extended Slices
I had never used extended slices with a third argument. The syntax is exactly: mylist[first:last:step]
which will produce mylist[first], mylist[first+step] … until mylist[last] not included.
Example:
- Extracting elements with even indexes:
mylist[::2]
- Reversing order:
mylist[::-1]
This works with lists, tupples and strings.
Ref: Understanding slice notation
Strings
- Important: keep in mind strings are immutable in Python. If you want to modify letters in a string, a common way is to transform them in a list, edit the list and join back.
a[5:]
works even if there are less than 6 characters in the string! It will return''
Map
Map applies a function to elements of an iterable object. If we have a line of numbers, the following works:
|
|
But it also works with map:
|
|
If you need to rule out some values, consider using filter
: filter(function, iterable)
.
|
|
Priority queue
|
|
A heap queue, also known as priority queue, will also keep the lowest value first:
|
|
Regex
I often need to match a given syntax and return only the part that matches. For example: Game 123
and I want to retrieve 123
.
The way to do that is with a search
and then use group
on the match.
|
|
Remember that search
only returns the first occurence. If you need them all, consider using findall
.
- Find 3 letters:
[A-Z]{3}
. This will matchABC
, orHSM
but notH8T
.
Reading a file
It’s not difficult to read a file line by line in Python. Pay attention that the following includes the trailing \n
at the end of each line.
|
|
If you don’t want those \n
, you can remove them or simpler:
|
|
Pointers
Pointers are essential to any C developper. There are no pointers in Python, but there are mutable objects and we can more or less simulate the mechanism.
In the following, seed_to_soil
and soil_to_fertilizer
are lists.
I use tags
as a pointer to the right list.
|
|
Unpacking operators
Functions such as math.lcm
use a variable number of arguments:
|
|
If we have a list of integers to provide, we can use the unpacking operator *
: math.lcm(*tab)
. Actually, *
works over any iterable object (even for example on strings themselves).
If you want a variable number of named arguments, use the unpacking operator **
. This create a dict that the function iterates over.
Zip
zip
takes one or multiple iterables as input. It returns a sequence of tuples with one element of each iterable.
This is quite useful in Day 14 to convert a matrix organized by lines, into a matrix of columns.
|
|
Note that zip
works without issue on iterables of different lengths. It will operate till the smallest iterable.
As mentioned in Real Python, zip
is also particularly interesting when you want to loop over several iterables:
|
|
Other good example in Real Python, using zip
to unzip with the unpacking operator:
|
|
Note that it’s possible to zip iterables of different lengths.
Underscore
There are more uses of underscore that I had ever imagined.
Basically, in [dict() for _ in range(256)]
the underscore acts as an “unnamed” variable.
It can also used to ignore values:
|
|
Misc
- Read a string right to left:
reversed(s)
- Is a digit:
isdigit()
- Sum all elements of a list:
sum(thelist)
- Lambda functions
- To compute combinations, use
from itertools import combinations
- Python 3.8 introduced
:=
which “assigns values as part of a larger expression”. In the following,n
gets the value oflen(a)
.
|
|
Elegant solutions (SPOILER ALERT)
Those are solutions I found on Internet. Not mine
Day | Link |
---|---|
Day 1 Task 1 | Awk often produces very short solutions |
Day 3 | Bash one-liner: cat input4.txt | cut -d: -f2 | tr -d '|' | xargs -I {} sh -c "echo {} | tr ' ' '\n' | sort | uniq -d | wc -l " | awk '$1>0 {print 2^($1-1)}' | paste -sd+ - | bc . The command paste merges lines of files |
Day 4 Task 2 | `cat /tmp/input4.txt |
Day 5 Task 2 | Super fast Perl implementation |
Day 8 | Blog post explaining the solution |
Day 14 | Video explaining the solution |
Day 15 | This solution uses the match statement, which is like C’s switch |
C
I solved a few challenges using plain C (day 5, day 6, day 7 for instance). If you’re not going to use some Python specificities, C does the work very well… and fast.
Performance
Python is way slower than C. That’s a fact. For example, this is the performance for similar implementations of Day 5 Task 2. The same with Python simply would not finish in a reasonable time.
Implementation | Time to solution |
---|---|
gcc d5t2.c -o d5t2 | 7 min 43 |
Optimized: gcc -O3 ... | 2 min 20 |
Java | 3 min 30 |
However… it depends very much on the algorithm you choose and your implementation skills…
In Python, if a given function is called many times with the same arguments, it’s a good idea to memoize it to speed up the result. This can be done with from functools import cache
and the @cache
decorator.
For example, this Day 12 solution caches the recurse
function. Beware, caching works with simple types, it won’t work with lists.
|
|
Also, if you’re using recursivity intensively, you might need the optimization of sys.setrecursionlimit(number)
- but that might not solve all your performance issues ;)