Advent of Code 2021 - Day 7

By Eric Burden | December 7, 2021

It’s that time of year again! Just like last year, I’ll be posting my solutions to the Advent of Code puzzles. This year, I’ll be solving the puzzles in Julia. I’ll post my solutions and code to GitHub as well. I had a blast with AoC last year, first in R, then as a set of exercises for learning Rust, so I’m really excited to see what this year’s puzzles will bring. If you haven’t given AoC a try, I encourage you to do so along with me!

Day 7 - The Treachery of Whales

Find the problem description HERE.

The Input - Nothing to See Here

Seriously. Today’s input is a comma-separated list of numbers. So…

function ingest(path)
    return open(path) do f
        readsplit(x) = split(readchomp(x), ",")
        [parse(Int, s) for s in readsplit(f)]
    end
end

See? Told you.

Part One - Crabwalk

For today’s puzzle, our job is to help a bunch of crabs in submarines (?) line up while expending as little energy as crabbily possible. Thankfully, since they’re crabs, they can only move sideways, so we only have one dimension to work with. Given our list of numbers, our job is to find a number such that the crabs, in aggregate, can line up on that number while moving as little as possible. Something about not having a lot of fuel in their little submarines, which you’d imagine would be a good thing to think about before you took it down to the ocean floor, but what do I know?

using Statistics

# The position that will be optimally close to each crab is
# the median of all the crab positions.
function part1(input)
    position = convert(Int, median(input))
    crabdistance = abs.(position .- input)  # Broadcasting!
    return sum(crabdistance)
end

This was less a programming challenge than a math/logic challenge. If it’s not clear why the median of the array of crab positions is the right value for them to line up on, consider the following sequence (our example) in order:

0, 1, 1, 2, 2, 2, 4, 7, 14, 16
              ^
            median

It makes intuitive sense that the optimal position should be somewhere near the middle, and there’s nowhere more “middle” than the median. A mean, or average, can be influence by large outlier values, but not the median. With this intuition in hand, try to come up with an example where the median wouldn’t be the right answer. I couldn’t, but maybe you’re more creative than I am.

Part Two - Give it the Gauss

Turns out, crab submarines burn fuel at an ever-increasing rate the longer they move. I mean, it’s good for us that the crabs are down here, but I don’t think they ever stood a chance of getting back to the surface. This part is more interesting, from a coding perspective:

using Base.Iterators
using Statistics 

# Overloading arbitrary operators is fun!
±(a, b) = (a + b, a - b)

# Use the Gaussian method for summing sequences of numbers to quickly calculate
# the total distance of all crabs to a given position.
function gaussdistance(position::Int, crabs::Vector{Int})::Int
    # So much broadcasting!
    n = abs.(crabs .- position) .+ 1
    distances = (n .* (n .- 1))  2
    return sum(distances)
end

# Makes a reasonable guess about where the optimal crab position is, then
# searches to the left or right to find the real optimal position.
function part2(input)
    # Start at a reasonable place, the average value of the crab positions
    startingat = trunc(Int, mean(input))
    crabdistance(x) = gaussdistance(x, input)
    currentmin = crabdistance(startingat)

    # Now we need to decide if we should search for a position to the left
    # or right of our starting position. Check to the left and right of the
    # starting position. We'll move in whichever direction has a lower
    # aggregate crab distance. If somehow our guess was spot on, we'll 
    # end up skipping the for loop below and just returning the total distance
    # to the starting position.
    (left, right) = startingat ± 1
    (leftdist, rightdist) = map(crabdistance, (left, right))
    
    (position, nextdist, step) = (
        leftdist < rightdist 
        ? (left, leftdist, -1) 
        : (right, rightdist, 1)
    )

    # Now just iterate in the direction we decided on above, until we find
    # the total distance starts to increase. At that point, we can return 
    # the minimum value found.
    while nextdist < currentmin
        currentmin = nextdist
        position += step
        nextdist = crabdistance(position)
    end

    # NOTE: For my input, the above loop never runs because the actual position
    # turned out to be one to the right of my starting position. Your mileage 
    # may vary.
    return currentmin
end

We can quickly calculate the amount of fuel a crab needs to move a given distance using the Gaussian Method. With that in place, we pick a spot on the number line to start our search for the optimal position. Since we know that somewhere on the number line there is a value that will yield the minimum crab fuel cost (and we can surmise that crab fuel costs rise to either side of that value), we can check to the left and right of our starting guess. Whichever side has a value lower than our initial guess is the direction we should go to find the minimum aggregate fuel cost. From there, it’s just a matter of trying values down the number line until the fuel cost starts to go back up again, then we’ll know we’ve found our ideal position.

Wrap Up

Today was all about broadcasting! And a bit of math. But mostly broadcasting! If you’re not familiar, broadcasting is the term used in Julia to describe the syntax for vectorizing functions and operations, named after the broadcast() function. That’s what all the dots before operators and after function names are about. Once you understand that 5 .+ [5, 10, 15] is [10, 15, 20], then the vectorized operations and functions can make your code a lot more readable. Using broadcasting eliminated a number of for loops in today’s code.

If you found a different solution (or spotted a mistake in one of mine), please drop me a line!

comments powered by Disqus