Skip to Content

Advent of Code 2024 - Day 7, in Kotlin - Bridge Repair

Kotlin solutions to parts 1 and 2 of Advent of Code 2024, Day 7: 'Bridge Repair'

Posted on

Today we’ll see the power of higher-order functions (the ability to pass functions as arguments to other functions, or to return functions from functions). Higher-order functions are a core concept when writing functional code.

If you’d rather just view the code, my GitHub Repository is here.

Puzzle Input

Today, we want to parse our input down to a List<List<Long>> where the inner list represents each Long on a row of input.

class Day07(input: List<String>) {

    private val equations: List<List<Long>> = input
        .map { row -> row.split("""\D+""".toRegex()).map { it.toLong() } }

}

To accomplish this, we’ll use a regular expression which reads as “Anything that isn’t a number” to split each row, mapping each of the parts toLong.

⭐ Day 7, Part 1

The puzzle text can be found here.

To solve Part 1, we’ll write a recursive function to perform all possible operations on the list of numbers until we either exceed the target (this attempt is a failure) or hit the target with no work remaining (success!). At each step, we will both add and multiply, branching off recursively in search of an answer.

To start, we’ll define some operators, which will be functions that take two Longs, do some calculation, and return a Long result. The two we need for Part 1 are addition and multiplication.

// In Day07

private val operators: List<(Long, Long) -> Long> = listOf(
    { a, b -> a + b },
    { a, b -> a * b }
)

Next, we’ll write a function to assist with calculating our answer. It takes a list of validOperators (foreshadowing!), passes each equation to a to-be-written hasSolution function, and gets the sumOf the equations that have solutions.

// In Day07

private fun solve(validOperators: List<(Long, Long) -> Long>): Long =
    equations
        .filter { hasSolution(validOperators, it[0], it[1], it.subList(2, it.size)) }
        .sumOf { it.first() }

I want to point out the use of subList here. This is nice because it doesn’t copy anything, it gives us a view over the underlying list. The only tricky part is that subList is exclusive meaning it can get into clubs I can’t we have to give it 1 past the last index we want, so we’ll use size here instead of lastIndex.

Now for the code that actually implements most of our strategy. The hasSolution takes a lot of arguments, so let’s go through them. First, the list of operators (again, foreshadowing!) followed by the target we’re trying to hit (the first number of the equation), the sum we’ve achieved so far, and how many numbers in the equation are remaining.

// In Day07

private fun hasSolution(
    operators: List<(Long, Long) -> Long>,
    target: Long,
    sum: Long,
    remaining: List<Long>
): Boolean =
    when {
        remaining.isEmpty() -> target == sum
        sum > target -> false
        else -> operators.any { operator ->
            hasSolution(
                operators,
                target,
                operator.invoke(sum, remaining[0]),
                remaining.subList(1, remaining.size)
            )
        }
    }

Remember the rule for writing a recursive function: Always define your end conditions first. In this case, if we don’t have any more numbers to operate on (the remaining list is empty) then we know we’ve got a valid solution if the target and the sum are equal. Otherwise sum is either higher or lower than target and we have no way to alter that.

The next end condition is if sum exceeds target. Since we don’t have any operators that will ever make sum smaller, we know for a fact we’ve overshot the target and can exit early.

All that remains is to test if any of our operators give us a solution. We use the any function from the Kotlin Standard Library to do this for us. It will stop evaluating possibilities as soon as one is true, or return false if none of them are. In this function we recursively call hasSolution with the same operators, the same target, a sum that is the result of the current operator invoked over the previous sum and the first element of the remaining numbers, and the rest of the remaining numbers.

Calling solve with our list of operators solves Part 1!

// In Day07

fun solvePart1(): Long =
    solve(operators)

Star earned! Onward!

⭐ Day 7, Part 2

The puzzle text can be found here.

To solve Part 2, we can reuse everything we wrote for Part 1. The only work to do is to add a new operator to concatenate two numbers. Rather than define a new list of operators for Part 2, we’ll use the operators list from Part 1 and concatenate (see what we did there?) our new operator to the end of the list before passing it to solve.

// In Day07

fun solvePart2(): Long =
    solve(operators + { a, b -> "$a$b".toLong() })

See the power of higher-order functions? We didn’t change anything except adding a new function to a list. That’s really powerful!

Star earned! See you tomorrow!

Further Reading

  1. Index of All Solutions - All posts and solutions for 2024, in Kotlin.
  2. My Github repo - Solutions and tests for each day.
  3. Solution - Full code for day 7
  4. Advent of Code - Come join in and do these challenges yourself!