Advent of Code 2024 Day 7 – Bridge Repair

Advent of Code 2024 Day 7 – Bridge Repair

- 16 mins

Day 7: Bridge Repair

Part 1

Today’s input is a list of equations where the operators are missing. An equation is of the form X: Y ... Z, where X is the value and Y ... Z could be combined in some way to get X. The example input is:

190: 10 19
3267: 81 40 27
83: 17 5
156: 15 6
7290: 6 8 6 15
161011: 16 10 13
192: 17 8 14
21037: 9 7 18 13
292: 11 6 16 20

Operators are always evaluated left-to-right, not according to precedence rules, and parentheses are not allowed. The operators we need to consider are only + and *. Some of the equations are wrong, i.e., they don’t evaluate to the value on the left side of the colon, no matter what combination of operators are used. As each equation is missing the operators, we need to figure out how to combine the numbers to get the value on the left side of the colon, if possible.

For example:

We need to sum the values on the left side of the colon for the equations that are actually solvable. So for the sample input, the answer is 190 + 3267 + 292 = 3749.

My Solution

My solution used a forward recursion approach at first, and then once I had a working solution I did a backward recursion approach as well.

Forward Recursion

Let’s start with the pseudocode first:

I can implement this in Python as follows:

def is_valid_equation(equation: Equation, operators) -> bool:
    desired_value, numbers = equation
    if numbers[0] > desired_value:
        return False
    if len(numbers) == 2:
        return any(desired_value == op(*numbers) for op in operators)
    first, second, *remaining_numbers = numbers
    return any(
        is_valid_equation(
            (desired_value, [op(first, second)] + remaining_numbers), operators
        )
        for op in operators
    )

This function takes an equation and a list of operators to apply to the numbers. Breaking down the function:

I can now use this function to find all the solvable equations and sum the values on the left side of the colon.

def main1_forward():
    with open("input.txt", "r") as f:
        equations = [
            (int(desired_value), list(map(int, remaining_numbers.split())))
            for line in f
            for desired_value, remaining_numbers in [line.split(":")]
        ]

    print(
        f"LOGF: Sum of true equations: {sum(equation[0] for equation in equations if is_valid_equation(equation, {operator.add, operator.mul}))}"
    )

This function reads the input, splits the equation into the desired value and the numbers, and stores them in a list. It then prints the sum of the values on the left side of the colon for the solvable equations. The operators are passed in as a set of operator.add and operator.mul.

This does work, but I’m brute-forcing all the possible combinations of the operators to solve the equations. I can find a better way to pick my operators.

Backward Recursion

Instead of looking at the first two numbers and applying all possible operators to them, I can instead look at the last number and the desired value to pick the operator in a smarter way.

Let’s explain the logic with an example, using an equation from the sample input, 292: 11 6 16 20.

This approach is more efficient than the forward recursion approach, as we don’t always check all possible combinations of operators, pruning the ones that are not possible.

I can implement this in Python as follows:

def is_valid_equation_p1(equation: Equation) -> bool:
    desired_value, numbers = equation
    if len(numbers) == 2:
        return (
            desired_value == numbers[0] + numbers[1]
            or desired_value == numbers[0] * numbers[1]
        )
    last = numbers[-1]
    mult, add = False, False
    if desired_value % last == 0:
        mult = is_valid_equation_p1((int(desired_value / last), numbers[:-1]))
    if desired_value - last >= 0:
        add = is_valid_equation_p1((desired_value - last, numbers[:-1]))
    return any([mult, add])

Breaking down the function:

I can now use this function to find all the solvable equations and sum the values of the left side of the colon, just like before.

def main1_backward():
    with open("input.txt", "r") as f:
        equations = [
            (int(desired_value), list(map(int, remaining_numbers.split())))
            for line in f
            for desired_value, remaining_numbers in [line.split(":")]
        ]

    print(
        f"LOGF: Sum of true equations: {sum(equation[0] for equation in equations if is_valid_equation_p1(equation))}"
    )

Looking at the performance of both the forward and backward recursion approaches:

LOG: Sum of true equations: 1153997401072
Function 'main1_forward' executed in 0.1072s
LOG: Sum of true equations: 1153997401072
Function 'main1_backward' executed in 0.0058s

We can see that the backward recursion approach is orders of magnitude faster than the forward recursion approach. I’m happy with the performance of my solution, and I can move on to part 2.

I’ve only included the relevant parts of the code here, but to see my full solution, you can check out my Advent of Code GitHub repository.

Part 2

For part 2, a new operator is introduced: ||. This is the concatenation operator. It combines the digits of the numbers, so for example 12 || 34 would be 1234.

The input for part 2 is the same as part 1, but with the new operator, more equations are now solvable.

With these new solvable equations, the new sum of the values on the left side of the colon is 11387.

My Solution

Forward Recursion

My forward recursion approach for part 1 is already set up to handle the new operator. I just need to add the new operator to the set of operators.

I need to create the new operator function and add it to the set of operators:

def concat_op(a, b):
    return int(str(a) + str(b))

All this function does is concatenate the two numbers as strings and converts the result to an integer.

I can now add the new operator to the set of operators and call the is_valid_equation function.

{sum(equation[0] for equation in equations if is_valid_equation(equation, {operator.add, operator.mul, concat_op}))}

Other than this, the rest of the code remains the same.

Backward Recursion

The backward recursion approach for part 2 is also similar to part 1, but I need to add the new operator within the function instead of passing it in.

I first make a helper function to check if the desired value ends with the last number:

def ends_with(a: int, b: int) -> int:
    str_a, str_b = str(a), str(b)
    if str_a.endswith(str_b):
        remaining = str_a[: -len(str_b)] if len(str_a) > len(str_b) else "0"
        return int(remaining)
    return 0

This function checks if the desired value ends with the last number. If it does, it returns the remaining part of the desired value after removing the last number. If it doesn’t, it returns 0. I can now use this function in my main validation function.

def is_valid_equation_p2(equation: Equation) -> bool:
    desired_value, numbers = equation
    if len(numbers) == 2:
        return (
            desired_value == numbers[0] + numbers[1]
            or desired_value == numbers[0] * numbers[1]
            or desired_value == concat_op(numbers[0], numbers[1])
        )
    last = numbers[-1]
    mult, add, concat = False, False, False
    if desired_value % last == 0:
        mult = is_valid_equation_p2((int(desired_value / last), numbers[:-1]))
    if desired_value - last >= 0:
        add = is_valid_equation_p2((desired_value - last, numbers[:-1]))
    if concat := ends_with(desired_value, last):
        concat = is_valid_equation_p2((concat, numbers[:-1]))
    return any([mult, add, concat])

Going over the additions from the part 1 validation function:

This function is called just like the part 1 version.

print(f"LOG: Sum of true equations with concat: {sum(equation[0] for equation in equations if is_valid_equation_p2(equation))}")

Again the performance of the backward recursion approach is much better than the forward recursion approach.

LOG: Sum of true equations with concat: 97902809384118
Function 'main2_forward' executed in 2.7434s
LOG: Sum of true equations with concat: 97902809384118
Function 'main2_backward' executed in 0.0161s

Again, I’ve only included the relevant parts of the code here, check out my repository for the full solution.


That’s it for day 7 of Advent of Code 2024! I hope you enjoyed reading my solution and let’s see how the rest of the month goes!

Vinesh Benny

Vinesh Benny

A Software Engineer learning about different things in life and otherwise