GUISSMO

Advent of Code 2023: Day 3

Here are the Day 3 puzzles for Advent of Code 2023.

Here’s my Day 1 blog post. Day 2 was not so remarkable so I didn’t bother posting about it. While this one isn’t much better, at least we could flex a bit some of our padding tricks, knowledge of classes, never-before-used(-by-me) regex functions, and a bonus section on mutability.

As before, here’s the code for my leaderboard: 1598637-22d94a1d. You use this link to add it once you’re logged in. And if you wake up earlier than me (which is probably the case), you’ll probably be higher up because the way they score the leaderboard is mental.

In the code below, sample_input is the array of lines that appear in the sample input.

One of the Oldest Tricks in The Book

With problems involving grid and adjacency, it’s always great to pad your grids. That’s exactly the first things I did.

def padded_grid(inp):
    pad = '.'*(len(inp[0])+2)
    mid = [f'.{i}.' for i in inp]
    return([pad] + mid + [pad])

def print_grid(inp):
    for i in inp:
        print(i)

Printing the grid…

print_grid(padded_grid(sample_input))

…we see a beautiful border around the input grid.

    ............
    .467..114...
    ....*.......
    ...35..633..
    .......#....
    .617*.......
    ......+.58..
    ...592......
    .......755..
    ....$.*.....
    ..664.598...
    ............

This is not completely necessary but this will save us from the many headaches of handling literal edge cases later.

Class Time

We class-ify our previous code… or should I say yassify our previous code… into this neat Grid class…

class Grid():

    def __init__(self, inp):
        self.grid = padded_grid(inp)

    def __str__(self):
        ret = ""
        for l in self.grid:
            ret += l
            ret += '\n'
        return ret

For my solution, we need to keep track of the values and the position. So it made sense to just add another straightforward Number class.

class Number():

    def __init__(self, num, i, j0, j1):
        self.value = int(num)
        self.i = i
        self.j0 = j0
        self.j1 = j1

    def __str__(self):
        return f'number {self.value} at row {self.i} spanning columns {self.j0} to {self.j1}'

And let’s make a new Symbol class while we’re at it.

class Symbol():

    def __init__(self, num, i, j):
        self.value = num
        self.i = i
        self.j = j

    def __str__(self):
        return f'symbol {self.value} at row {self.i} and column {self.j}'

On pars(e)

I didn’t bother writing about the Day 2 puzzles because it was mostly parsing and less problem solving.

This was a bit like that too, but at least this had something to teach me about regex. Apparently, re has this finditer function that I never really needed before. But it’s handy for this problem so I’ll take that excuse to use it.

import re

class Grid():

    def __init__(self, inp):
        # ... #
        self.numbers = self.detect_numbers()

    #...#

    def detect_numbers(self):
        ret = []
        for i,l in enumerate(self.grid):
            for m in re.finditer(r'[0-9]+', l):
                (j0, j1) = m.span()
                ret.append(Number(m[0], i, j0, j1-1))
        return ret

This code works, but I got burned by an off-by-one error today because I previously didn’t put a -1 on j1 in Number(m[0], i, j0, j1-1).

I then copy-pasted detect_numbers and wrote detect_symbols. If you’re asking why I didn’t refactor this reused code, it is summarized in this concise explanation. Of course, if I had to maintain this codebase in the future, maybe I would have.

Part 1

All that’s left to finish part 1 of the problem is to detect adjacent symbols — which boils down to figuring out if any of the given symbols is in the “padded rectangle” around a Number.

class Grid():

    #...#

    def detect_adjacent_symbols(self, num):
        ret = []
        for s in self.symbols:
            if num.i-1 <= s.i and s.i <= num.i+1 and num.j0-1 <= s.j and s.j <= num.j1+1:
                ret.append(s)
        return ret

    def sum_of_part_numbers(self):
        ret = 0
        for n in self.numbers:
            if len(self.detect_adjacent_symbols(n)) > 0:
                ret += n.value
        return ret

Once we have that function, we simply add all numbers with at least one adjacent symbol to the sum. And voila, we have:

grid = Grid(sample_input)
print(grid.sum_of_part_numbers())

And get:

4361

Part 2

I didn’t know what Part 2 of this problem would be a priori but since we’ve structured our code nicely, we just needed to add a few more functions to finish it off.


class Grid():

    # ... #

    def find_gear_ratio(self):
        dic = {}
        for n in self.numbers:
            adjacent_symbols = self.detect_adjacent_symbols(n)
            for s in adjacent_symbols:
                if s.value != '*':
                    continue
                k = f'{s.i},{s.j}'
                if k not in dic:
                    dic[k] = []
                dic[k].append(n)

        ret = 0
        for k in dic:
            l = dic[k]
            if len(l) == 2:
                ret += l[0].value*l[1].value
        return ret

grid = Grid(sample_input)
grid.find_gear_ratio()
467835

I Just Want Your Extra Time and Your (Immutable) Keys

While writing this blog post, I noticed how artificial this line was:

k = f'{s.i},{s.j}'

I vaguely remember from “Python school” (i.e. watching video tutorials) that you can use any immutable object in Python as a dictionary key. I tried that while coding, but ended up with an error. It turns out that the error was something else, and it is perfectly safe to just use tuples as keys.

k = (s.i, s.j)

A list can’t be a key though! Their value can be changed via a .append — a reminder that they are mutable.

Contact

Feel free to contact me to let me know how you did!

Back to Top | Blog RSS Feed