Imports

Code
import dataclasses
import functools
import itertools
import os
import re
import sys
from collections import defaultdict, deque, namedtuple
from queue import PriorityQueue

import more_itertools
import numpy as np
import pandas as pd
import scipy

sys.path.insert(1, "..")

import utils

load = utils.year_load(2024)

Day 1: Historian Hysteria

Part 1

Load the data, sort the two columns, find the absolute difference between them and sum the result.

Code
data = np.sort(load(1, "int"), axis=0)
abs(np.diff(data)).sum()

Part 2

numpy has a handy method to find the unique values in an array, and their counts. Let’s use that to make a dictionary of value -> count pairs for the right list with a default value of zero, and then just look up that value for each element of the left list.

Code
lookup = defaultdict(int)
lookup.update(
    {value: count for value, count in zip(*np.unique(data[:, 1], return_counts=True))}
)
sum(x * lookup[x] for x in data[:, 0])

Day 2: Red-Nosed Reports

Part 1

Nothing too complicated going on here: load the data, and find the difference between successive values. To account for decreasing sequences, multiply by the sign of the first difference and then require that all the differences be greater than one and less than or equal to three.

Code
data = load(2, "int")


def is_safe(line):
    diff = np.diff(line)
    diff = diff * np.sign(diff[0])
    return int(((diff > 0) & (diff <= 3)).all())


sum(is_safe(line) for line in data)

Part 2

I spent a bit of time trying to see if there was a neat way of incorporating the “is valid if any one number is deleted” requirement, but I couldn’t immediately see it, so I ended up just iterating over all the possible deletions instead.

Code
total = 0
for line in data:
    for idx in range(len(line)):
        if is_safe(line[:idx] + line[idx + 1 :]):
            total += 1
            break
total

Day 3: Mull It Over

Part 1

We get to play with regex! Well, I do – there might be better ways of tackling this. The format for a valid mul instruction is quite strict, encoding that as a regex is fairly straightforward. Once we have that, we can use re.findall to find all the occurrences and extract the integers.

Code
data = load(3, "raw")
mul = r"mul\((\d{1,3}),(\d{1,3})\)"
sum(int(pair[0]) * int(pair[1]) for pair in re.findall(mul, data))

Part 2:

I’m pretty happy with my part two, which I managed to do fairly elegantly. We can ignore all the sections immediately after a don't() instruction by splitting the string on those, and then discarding the start of each substring up to the first do() instruction. Concatenating all the substrings gives us a clean string with just the segments we are interested in, and we can proceed as before.

Code
clean = "".join(
    [segment[segment.find("do()") :] for segment in ("do()" + data).split("don't()")]
)
sum(int(pair[0]) * int(pair[1]) for pair in re.findall(mul, clean))