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 2: math.prod - like
sum
for products!
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.