Logo

dev-resources.site

for different kinds of informations.

How to repair a bridge, Advent of Code 2024 Day 7

Published at
12/30/2024
Categories
functionalprogrammin
python
adventofcode
Author
jeffrey04
Author
9 person written this
jeffrey04
open
How to repair a bridge, Advent of Code 2024 Day 7

After what felt like an eternity (five hours, to be precise), Day 20 part 2 finally decided to cooperate. I’m still slightly dazed from the wait, but duty calls! Today we’re tackling Day 7 of Advent of Code, picking up where we left off with Day 6 last week. Our task today is to repair a bridge so we can cross it and continue our search for the Chief Historian.


A cute illustration generated by Microsoft Copilot

Today’s challenge presents us with a different kind of problem: we’re given lists of numbers arranged in a specific format (thankfully, not a 2D puzzle today…). Each line is designed to form an equation, but the operators are absent. Our task is to test various operators to determine if the resulting equation holds true.

We use two parsing functions to process the input. The parse function first splits each line by the colon (:) character:

def parse(input: str) -> tuple[tuple[int, tuple[int, ...]], ...]:
    return tuple(
        parse_line(*line.strip().split(":")) for line in input.strip().splitlines()
    )
Enter fullscreen mode Exit fullscreen mode

parse_line converts the string expected and the string of operands to integers, returning a tuple containing the integer expected and a tuple of integer operands

def parse_line(expected: str, operands: str) -> tuple[int, tuple[int, ...]]:
    return int(expected), tuple(int(item) for item in operands.strip().split(" "))
Enter fullscreen mode Exit fullscreen mode

I prefer a functional programming style, and despite Python’s imperative nature, libraries like toolz/cytoolz are incredibly useful. Today, we’re using thread_first from toolz.functoolz. Here's how thread_first operates: It takes an initial value and then applies a sequence of function-argument pairs, threading the result through each step.

>>> from operator import add, mul
>>> assert thread_first(1, (add, 2), (mul, 2)) == mul(add(1, 2), 2)
Enter fullscreen mode Exit fullscreen mode

In this example, thread_first starts with 1, then applies add with 2 (resulting in 3), and finally applies mul with 2 (resulting in 6). This is equivalent to mul(add(1, 2), 2).

We now define the calculate function to apply the operations. It takes a tuple of functions (funcs) and a tuple of operands (operands) as input:

def calculate(
    funcs: tuple[Callable[[int, int], int], ...], operands: tuple[int, ...]
) -> int:
    assert len(operands) - len(funcs) == 1

    return thread_first(operands[0], *(zip(funcs, operands[1:])))
Enter fullscreen mode Exit fullscreen mode

The assert ensures one more operand than functions. operands[1:] provides the operands for the functions. zip and * create the function-operand pairs for thread_first, which performs the chained calculations.

Example:

>>> from operator import add, mul
>>> calculate((add, mul), (2,3,4))
20
Enter fullscreen mode Exit fullscreen mode

Now we can verify each line of the input using the check_can_calibrate function. This function takes the expected result, the operands, and a tuple of possible functions as input, and returns True if any combination of functions produces the expected result, and False otherwise:

def check_can_calibrate(
    expected: int,
    operands: tuple[int, ...],
    funcs: tuple[Callable[[int, int], int], ...],
) -> bool:
    return next(
        filter(
            None,
            (
                calculate(funcs, operands) == expected
                for funcs in product(funcs, repeat=len(operands) - 1)
            ),
        ),
        False,
    )
Enter fullscreen mode Exit fullscreen mode

itertools.product generates all function combinations. A generator expression checks if any combination matches the expected result. filter(None, ...) and next(..., False) efficiently find the first True result or return False if none is found.

For Part 1, we’re given only multiplication and addition operators. The puzzle asks for the sum of the expected values, where a valid equation can be formed using these operators. We implement the evaluate function to calculate this sum:

def evaluate(
    input_parsed: tuple[tuple[int, tuple[int, ...]], ...],
    funcs: tuple[Callable[[int, int], int], ...],
):
    return sum(
        expected
        for expected, operands in input_parsed
        if check_can_calibrate(expected, operands, funcs)
    )
Enter fullscreen mode Exit fullscreen mode

It iterates through the parsed input and sums the expected values for which check_can_calibrate returns True.

Lastly, we assemble part 1 from what we have already built so far

import operator

def part1(input: str) -> int:
    return evaluate(parse(input), (operator.mul, operator.add))
Enter fullscreen mode Exit fullscreen mode

This function parses the input using parse and then calls evaluate with the parsed data and the tuple of functions (operator.mul, operator.add), representing multiplication and addition respectively.

In Part 2, we encounter a concatenation operator that joins two numbers together. In Python, this is equivalent to using an f-string:

>>> int(f"{20}{24}")
2024
>>> int(f"{123}{45}")
12345
Enter fullscreen mode Exit fullscreen mode

This effectively creates a new number by appending the digits of the second number to the end of the first number.

Alternatively, we can perform the concatenation using a mathematical formula. The number of digits in a positive integer x can be calculated using the formula:

This works because log₁₀(x) gives the power to which 10 must be raised to get x. The floor function rounds this down to the nearest integer, and adding 1 gives the number of digits. Let’s take 123 as an example:

Implementing this as the int_concat function:

def int_concat(alpha: int, beta: int) -> int:
    return alpha * (10 ** floor(log10(beta) + 1)) + beta
Enter fullscreen mode Exit fullscreen mode

Performing integer concatenation mathematically avoids the overhead of string conversions. String concatenation involves memory allocation and manipulation, which is less efficient than direct integer arithmetic, especially for large numbers or many concatenations. This mathematical approach is therefore generally faster and more memory-efficient.

Lastly, we implement part 2. The only difference compared to part 1 is the addition of the int_concat operator:

def part2(input: str) -> int:
    return evaluate(parse(input), (operator.mul, operator.add, int_concat))
Enter fullscreen mode Exit fullscreen mode

Ta-da! We’ve cracked Day 7. This was a relatively straightforward challenge, especially compared to the later days (Day 20 optimization is still giving me a headache 😭). Although it might not be the most performant, I prioritized readability.

That’s all for today. Happy holiday and happy new year 🎉! Fingers crossed for a better job situation next year (still #OpenToWork, ping me for collaborations!), and I shall write again, next week.

adventofcode Article's
30 articles in total
Favicon
Climbing a depth-first search hill, Advent of Code 2024, day 10
Favicon
Hoof It
Favicon
How to parse computer code, Advent of Code 2024 day 3
Favicon
Disk Fragmenter
Favicon
How to repair a bridge, Advent of Code 2024 Day 7
Favicon
Resonant Collinearity
Favicon
Advent of Code 2024 - Day 23: LAN Party
Favicon
How to trace steps in a map, Advent of Code 2024 day 6
Favicon
Advent of Code 2024 - Day 21: Keypad Conundrum
Favicon
Advent of Code 2024 - Day 18: Ram Run
Favicon
Advent of Code 2024 - Day 17: Chronospatial Computer
Favicon
Guard Gallivant
Favicon
Advent of Code 2024 Day 5 in Golang: Ordering Pages
Favicon
Advent of Code '24 - Day9: Disk Fragmenter (Python)
Favicon
Advent of Code #8 (in Gleam)
Favicon
Advent of Code #7 (in Gleam)
Favicon
Advent of Code #6 (in Gleam)
Favicon
Advent of Code #5 (in Gleam)
Favicon
AoC 24 - Day 4: Ceres Search
Favicon
Wednesday Links – Edition 2024-12-04
Favicon
Mull It Over
Favicon
Bridge Repair
Favicon
Advent of Code #3 (in Gleam)
Favicon
Day 3: Mull It Over | Advent of Code 2024 | Swift | 中文
Favicon
Red-Nosed Reports
Favicon
Day 1: Historian Hysteria | Advent of Code 2024 | Swift | 中文
Favicon
Advent of Code 2024 - Day 19: Linen Layout
Favicon
Advent of Code - It's More Than a Competition
Favicon
Advent of Code '24 - Day 10: Hoof It
Favicon
Advent of Code 2024 Retro: What could you do if you didn't care whether you failed?

Featured ones: