Invisible link to canonical for Microformats

Advent of Code 2023


A new problem, every day, all of december.


01 dec 2023

Advent of Code 2023

Last year I posted about 16 problems from Advent of Code 2022. This year, I will do Advent of Code 2023 in vanilla (only included libraries) python 3.11. I’ve joined the challenge a few times before - but never finished. Let’s see how many days I manage this round :D

Update: the answer is 4, December is such a busy month :)

NOTE: This blog contains spoilers - use it responsibly.

I’ve hidden the details for each day behind expand details blocks if you want to see only some solutions.

While my solutions do not use external libraries, I sometimes add graphics made with matplotlib.

Template

In general, these tasks require input data to be read in and be processed in two different parts. Here is the python template I use to get started:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Advent of code day XX - """
from pathlib import Path


def read_data(path: Path) -> list:
    """Read line data."""
    with path.open("r") as f:
        return [i.strip() for i in f.readlines()]

def part1(data: list[str]):
    pass


def part2(data: list[str]) -> int:
    pass


if __name__ == "__main__":
    DAY = "XX"
    exdata = read_data(Path(f"day{DAY}_example.txt"))
    indata = read_data(Path(f"day{DAY}_input.txt"))

    print("PART 1")
    print(f"\texample: {part1(exdata)}")
    print(f"\tinput: {part1(indata)}")

    print("PART 2")
    print(f"\texample: {part2(exdata)}")
    print(f"\tinput: {part2(indata)}")

I’ll keep my code on this github repository, most of the code will appear in this blog post.

Learnings

I learned a few useful things on these challenges so far:

Day 1: Trebuchet?!

The task is to add numbers from a calibration file together. Each row of the file encodes a 2-digit number with the first and last single digit in the row.

Example input data

1abc2
pqr3stu8vwx
a1b2c3d4e5f
treb7uchet

So the total should be 12+38+15+77=142.

In part 2, numbers can also be spelled out: one, two, three, four, five, six, seven, eight, and nine.

Example input data, Part B:

two1nine
eightwothree
abcone2threexyz
xtwone3four
4nineeightseven2
zoneight234
7pqrstsixteen

So the total should be 29+83+13+24+42+14+76=281.

Click to expand day 1 solution
 

Part 1 is straightforward. Just extract the digits, scale the first digit by 10 and add to the second one. For part 2, there can be overlaps like ‘eightwothree’, to get the correct result i replace numbers with first letter, number, last letter, so that: eight becomes e8t.

from pathlib import Path


def read_data(path: Path) -> list:
    """Read line data."""
    with path.open("r") as f:
        return [i.strip() for i in f.readlines()]


def parse(data: list[str]) -> list[int]:
    result = []
    for row in data:
        digits = [int(r) for r in row if r.isdigit()]
        result.append(digits[0] * 10 + digits[-1])
    return result


def part1(data: list[str]) -> int:
    return sum(parse(data))


def translate_digits(data: list[str]) -> list[str]:
    result = []
    for row in data:
        for i, a in enumerate(
            (
                "one",
                "two",
                "three",
                "four",
                "five",
                "six",
                "seven",
                "eight",
                "nine",
            ),
            1,
        ):
            row = row.replace(a, f"{a[0]}{i}{a[-1]}")
        result.append(row)
    return result


def part2(data: list[str]) -> int:
    return part1(translate_digits(data))


if __name__ == "__main__":
    DAY = "01"
    exdata = read_data(Path(f"day{DAY}_example.txt"))
    indata = read_data(Path(f"day{DAY}_input.txt"))

    print("PART 1")
    print(f"\texample: {part1(exdata)}")
    print(f"\tinput: {part1(indata)}")

    exdata_b = read_data(Path(f"day{DAY}_example_b.txt"))

    print("PART 2")
    print(f"\texample: {part2(exdata_b)}")
    print(f"\tinput: {part2(indata)}")

Day 2 - Cube Conundrum

Each game is a row in the input. It consists of several sets separated by;. A set has a , separated list of count and colour.

Example input data:

Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green

In part 1, we need add the game numbers together for games where all sets satisfy red<=12, green<=13, and blue<=14. In part 2, we need to add up the products of the maximum over each colour for all the sets in every game.

Click to expand day 2 solution
 

The decided to parse the data into r,g,b tuples. Then, I added a function to find max(r),max(g),max(b) over a list of tuples. For part 2, I found out about math.prod which works like sum but for products!

from pathlib import Path
import math


def read_data(path: Path) -> list:
    """Read line data."""
    with path.open("r") as f:
        return [i.strip() for i in f.readlines()]


def parse(data: list[str]) -> list[list[tuple[int, ...]]]:
    """Turn input rows into list of rgb tuples."""
    result = []
    for game in data:
        subsets = []
        for sub in game.split(":")[1].split(";"):
            cubes = {s.split()[-1]: int(s.split()[0]) for s in sub.split(",")}
            subsets.append(tuple(cubes.get(col, 0) for col in ("red", "green", "blue")))
        result.append(subsets)
    return result


def max_rgb(game: list[tuple[int, ...]]) -> tuple[int, ...]:
    """Find max of rgb tuples."""
    return tuple(max(game, key=lambda t: t[i])[i] for i in range(len(game[0])))


def part1(data: list[str], limit: tuple[int, ...] = (12, 13, 14)):
    return sum(
        igame * all((i <= c) for i, c in zip(max_rgb(game), limit))
        for igame, game in enumerate(parse(data), 1)
    )


def part2(data: list[str]):
    return sum(math.prod(max_rgb(game)) for igame, game in enumerate(parse(data), 1))


if __name__ == "__main__":
    DAY = "02"
    exdata = read_data(Path(f"day{DAY}_example.txt"))
    indata = read_data(Path(f"day{DAY}_input.txt"))

    print("PART 1")
    print(f"\texample: {part1(exdata)}")
    print(f"\tinput: {part1(indata)}")

    print("PART 2")
    print(f"\texample: {part2(exdata)}")
    print(f"\tinput: {part2(indata)}")

Day 3 - Gear Ratios

In part 1 we search an array with numerics, dots and other charactes for integers that are connected to a character that is not a dot. In part 2, we search for pairs of numbers connected to the same * character.

Example input

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

Here, 114 and 58 have no connections, and only (467,35) and (755,598) are connected by a *.

Click to expand day 3 solution
 

This was a little tricky. I decided to find the connection points and collect a list of all numbers connected to them.

from pathlib import Path
import itertools
import math


def read_data(path: Path) -> list:
    """Read line data."""
    with path.open("r") as f:
        return [i.strip() for i in f.readlines()]


def checks(data: list[str], x: int, y: int, chr=None) -> tuple[int, int] | None:
    for dx, dy in itertools.product([-1, 0, 1], [-1, 0, 1]):
        try:
            c = data[y + dy][x + dx]
        except IndexError:
            continue
        if c == chr if chr else not (c.isnumeric() or c == "."):
            return (x + dx, y + dy)
    return None


def find_connections(data: list[str], chr=None) -> dict[tuple[int, int], list[int]]:
    connections = {}
    conn = None
    for y, row in enumerate(data):
        number = ""
        for x, c in enumerate(row):
            if c.isnumeric():
                number += c
                conn = checks(data, x, y, chr) or conn
            if (x == len(row) - 1) or not c.isnumeric():
                if number and conn:
                    connections[conn] = connections.get(conn, []) + [int(number)]
                conn = None
                number = ""
    return connections


def part1(data: list[str]) -> int:
    return sum(sum(find_connections(data).values(), []))


def part2(data: list[str]) -> int:
    connections = {
        k: v for k, v in find_connections(data, chr="*").items() if len(v) == 2
    }
    return sum(math.prod(v) for v in connections.values())


if __name__ == "__main__":
    DAY = "03"
    exdata = read_data(Path(f"day{DAY}_example.txt"))
    indata = read_data(Path(f"day{DAY}_input.txt"))

    print("PART 1")
    print(f"\texample: {part1(exdata)}")
    print(f"\tinput: {part1(indata)}")

    print("PART 2")
    print(f"\texample: {part2(exdata)}")
    print(f"\tinput: {part2(indata)}")

Day 4 - Scratchcards

We get a list of cards like this

Card 1: 41 48 83 86 17 | 83 86  6 31 17  9 48 53

we need to consider the number of matches between the two lists separated by |. In part 1, we count a score based on the number of matches. In part 2, we get copies of the next number matches cards added to the stack.

Click to expand day 4 solution
 

I used a regexp match all to parse the lines. I first parsed everything and then made a match counter. In principle it would be more efficient to just insert that into the parser, but I didn’t know I wouldn’t need the paresed lists before part 2. For part 2 I just keep track of the number of instances of each card.

from pathlib import Path
import re


def read_data(path: Path) -> list:
    """Read line data."""
    with path.open("r") as f:
        return [i.strip() for i in f.readlines()]


def parse(data: list[str]) -> list[tuple[int, set[int], set[int]]]:
    pattern = r"Card\s+(\d+):\s*(.+?)\s*\|\s*(.+)"
    result = []
    for line in data:
        match = re.findall(pattern, line)
        card, a, b = match[0]
        result.append((int(card), set(map(int, a.split())), set(map(int, b.split()))))
    return result


def wins(parsed: list[tuple[int, set[int], set[int]]]) -> list[tuple[int, int]]:
    return [(i, len(w.intersection(n))) for i, w, n in parsed]


def part1(data: list[str]):
    return sum(int(2 ** (w - 1)) for i, w in wins(parse(data)))


def part2(data: list[str]):
    cards = {k: {"wins": v, "count": 1} for k, v in wins(parse(data))}
    for i, c in cards.items():
        for j in range(i + 1, i + 1 + c["wins"]):
            cards[j]["count"] += c["count"]
    return sum(c["count"] for i, c in cards.items())


if __name__ == "__main__":
    DAY = "04"
    exdata = read_data(Path(f"day{DAY}_example.txt"))
    indata = read_data(Path(f"day{DAY}_input.txt"))

    print("PART 1")
    print(f"\texample: {part1(exdata)}")
    print(f"\tinput: {part1(indata)}")

    print("PART 2")
    print(f"\texample: {part2(exdata)}")
    print(f"\tinput: {part2(indata)}")

The rest of the days

I did not have the time to go on this year, maybe I will have better luck in 2024.


Related