Skip to main content
  1. Posts/

5 Python tricks I learned from Advent of Code 2022

·899 words·5 mins

As someone who loves to practice code optimization techniques, December has become one of my favourite times of the year. Like last year, I participated in Advent of Code. This is an annual set of Christmas-themed programming puzzles that follow an advent calendar. This is the second time I participated, because it is a great way to practice my coding skills on problems I do not normally encounter.

One of my favourite things about it is that everyone is sharing their solutions on reddit or other forums. I love to share my own tricks, and even more to read about how others approached the same problem. I often go back to improve my own solutions to implement what I learned.

This is a short list of some of the tricks that I picked up this year.

Count #

Many puzzles require you to keep of some round or iteration number. While you could manually increment n in a while-loop, you can also iterate over count(). I used this in day 23 and day 24.

1
2
3
4
5
6
7
8
>>> from itertools import count
>>> for n in count():
>>>     if n == 3:
>>>         break
>>>     print(n)
1
2
3

This is especially useful to factor out the loop for some solutions. For day 23 I used this pattern to re-use the same code for both part 1 and 2:

1
2
3
4
5
def part1(s: str):
    return solve(s, rounds=range(10))

def part2(s: str):
    return solve(s, rounds=count(1))

Divmod #

Divmod is a python builtin that does division and modulo in a single function. It literally returns the (x//y, x%y):

1
2
>>> divmod(7,2)
(3, 1)

Cool! I used this in day 17 and 25 to save a few lines of code.

Complex numbers are magic #

Complex numbers can be used to store coordinates. Who knew!? I saw many solutions using this trick in combinations with sets hack to avoid having to set up a custom Pointer(x, y) class or (x, y) tuples. Where the real and imaginary components represent the x and y coordianates.

Complex numbers have 3 distinct advantages:

  • Complex numbers are immutable, so they can be added to sets (tuples or dataclasses cannot).
1
2
3
>>> coords = {1j, 1+2j, 2+3j}
>>> 1+2j in coords
True
  • They are a builtin type. This makes using them in list comprehensions a breeze.
1
2
3
>>> pos = 1+1j
>>> for new_pos in (pos + 1j, pos - 1 + 1j, pos + 1 + 1j):
>>>     ...
  • Complex math is really interesting for coordinate systems. Complex numbers are almost a coordinate system in itself. For example, a clockwise rotation is simply a multiplication by -1j:
1
2
3
4
5
6
7
>>> a = 2 + 1j
>>> for x in range(4):
>>>      print(a := a * -1j)
(1-2j)
(-2-1j)
(-1+2j)
(2+1j)

After seeing some solutions use this I tried it myself during day 14. While it made the code shorter, I thought it was a bit challenging to reason about it for myself. I tried again in some later solutions, but I prefer to pass row and column ((r, c)) tuples.

Sets are useful as sparse matrices #

As someone with a background in image processing, I immediately reach for numpy whenever something can be parsed into an array. This turned out to be the wrong choice on day 14 and 23.

In both cases, the array could be infinitely expanded which made numpy the wrong choice. In fact, in both puzzles, you could simply keep track of the ‘blocked’ positions as a set of coordinates. Bonus: checking whether coordinates are in a set is really fast in Python.

For example, this snippet from my solution for day 24:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> for rnd in count(1):
>>>     gusts = its_gusty_here(gusts)
>>>     gust_map = sum(gusts)
>>>
>>>     new_safe = {start}
>>>
>>>     for pos in safe:
>>>         new_safe |= set(is_safe(gust_map, pos))
>>>
>>>     safe = new_safe
>>>
>>>     if stop in safe:
>>>         break

for/else #

for/else and while/else are odd statements in Python. If it were up to Guido, he would not have included them at all. Their use is highly discouraged, because it’s somewhat confusing what to do. In my head I read it as for/nobreak. However, they can be useful on some occasions. This advent of code I found two perfect opportunities to use them.

1
2
3
4
5
6
for x in range(5):
    if some_condition(x):
        break
else:
    # loop completed
    assert x == 4

For example, I used this in my solutions for day 14:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
while hole not in cave:
    for new_pos in (grain + 1j, grain - 1 + 1j, grain + 1 + 1j):
        if part1 and (new_pos.imag == abyss):
            return n_grains
        elif new_pos.imag == abyss + 1:
            pass
        elif new_pos not in cave:
            grain = new_pos
            break
    else:
        cave.add(grain)
        n_grains += 1
        grain = hole

and day 23:

1
2
3
4
5
6
7
8
9
for pos in field:
    if not all_empty(field, pos):
        for func in moves:
            if new_pos := func(field, pos):
                break
        else:
            new_pos = pos
    else:
        new_pos = pos

Conclusion #

There you have it, some of the tricks I picked up during advent of code. See you next year! 🎄