Advent of Code - Day 6

By Eric Burden | December 6, 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 6 - Lanternfish

Find the problem description HERE.

The Input - School is in Session

Today’s puzzle has us calculating the number of lanternfish over time, with a given spawn rate. The key insight here is that there are 9 possible ages of fish (0-8), and each fish gets ‘reset’ to age 6 after spawning a new fish. New fish are spawned at age 8. Because we are interested in the total population of fish at the end of some time period, there’s no need to track each fish individually. So, for our input parsing, we count the number of fish who are initially at each possible age and return a vector of length 9, where each index contains the count of fish whose number of remaining days until spawning is index - 1 (Julia is 1-indexed).

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

    # Instead of reporting back the fish individually, return a
    # Vector of length `9` where each index represents the number 
    # of fish of age `idx - 1` . (Julia is 1-indexed)
    groups = zeros(Int64, 9)
    for f in fish
        groups[f+1] += 1
    end
    return groups
end

The Solution - My One and Only

For reasons that will become clear shortly, there’s really not a Part One and Part Two to today’s puzzle. Because the fish are spawning at a constant rate, we can keep track of groups of fish by the number of days they have before spawning. Each day, every fish’s number of remaining days is reduce by one, except for fish whose remaining days are 0. These fish will have their remaining days reset to 6, and they will also each produce a fish with remaining days of 8. So, we can essentially just rotate our array to the left, and add the value of groups[9] (the new fish spawned) to groups[7] (the number of fish who spawned).

# Create an Iterator over Generations of Fish Schools -------------------------
struct School
    agegroups::Vector{Int64}
end

# This function is called for the first iteration
function Base.iterate(iter::School)
    groups = copy(iter.agegroups)
    (sum(groups), groups)
end

# This function is called for each subsequent iteration
# Instead of keeping track of each fish and its progeny, we group all
# the fish by age and calculate the size of the next generation. Each
# generation/iteration creates `groups[1]` new fish at age `8` and rotates
# the group counts one to the left (such that the fish that were age `2` 
# are now age `1`)
function Base.iterate(iter::School, groups::Vector{Int64})
    groups = circshift(groups, -1)
    groups[7] += groups[9]
    (sum(groups), groups)
end

# Used to get the `nth` generation of a school. 
function Base.getindex(school::School, i::Int)
    for (generation, groups) in enumerate(school)
        generation > i && return groups
    end
end


# Solve the puzzle ------------------------------------------------------------

# Solve the puzzle, creating an iterator over generations of a 
# `School` and summing the values for a given day.
function solve(input, days)
    school = School(input)
    return sum(school[days])
end

Like yesterday, I found it handy to implement some of the iterator interface for School, a struct containing only the agegroups array. I also discovered I could implement getindex() as a method to get the nth element from the School iterator, which could technically iterate forever (or until the values overflowed). This works by iterating School forward the indicated number of ‘days’ and taking the last result.

Part one asks us to determine the count of fish at 80 days, and part two asks for the same thing at 256 days, which means making no change other than the second argument to solve().

Wrap Up

I spent WAY too much time today trying to figure out how to get the nth value from an iterator. I’m still not sure my result is the most idiomatic way of handling this in Julia. In Rust, there’s an nth() function for iterators that does this, and I was looking for something similar in Julia. I am very happy with how this code came out, but I do find myself wishing the documentation were organized differently. Or, maybe it’s just that I don’t understand Julia well enough yet, which is probably it. Either way, I understand it a bit better today that I did yesterday, which is really the point of all this. So, a win!

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

comments powered by Disqus