Powered by AppSignal & Oban Pro

Advent of Code - Day 6: Lanternfish

lib/advent_of_code/2021/day-06.livemd

Advent of Code - Day 6: Lanternfish

Mix.install([:kino])

Day 6: Lanternfish

Part One: Prompt

The sea floor is getting steeper. Maybe the sleigh keys got carried this way?

A massive school of glowing lanternfish swims past. They must spawn quickly to reach such large numbers - maybe exponentially quickly? You should model their growth rate to be sure.

Although you know nothing about this specific species of lanternfish, you make some guesses about their attributes. Surely, each lanternfish creates a new lanternfish once every 7 days.

However, this process isn't necessarily synchronized between every lanternfish - one lanternfish might have 2 days left until it creates another lanternfish, while another might have 4. So, you can model each fish as a single number that represents the number of days until it creates a new lanternfish.

Furthermore, you reason, a new lanternfish would surely need slightly longer before it's capable of producing more lanternfish: two more days for its first cycle.

So, suppose you have a lanternfish with an internal timer value of 3:

  • After one day, its internal timer would become 2.
  • After another day, its internal timer would become 1.
  • After another day, its internal timer would become 0.
  • After another day, its internal timer would reset to 6, and it would create a new lanternfish with an internal timer of 8.
  • After another day, the first lanternfish would have an internal timer of 5, and the second lanternfish would have an internal timer of 7.

A lanternfish that creates a new fish resets its timer to 6, not 7 (because 0 is included as a valid timer value). The new lanternfish starts with an internal timer of 8 and does not start counting down until the next day.

Realizing what you're trying to do, the submarine automatically produces a list of the ages of several hundred nearby lanternfish (your puzzle input). For example, suppose you were given the following list:

3,4,3,1,2 This list means that the first fish has an internal timer of 3, the second fish has an internal timer of 4, and so on until the fifth fish, which has an internal timer of 2. Simulating these fish over several days would proceed as follows:

Initial state: 3,4,3,1,2
After  1 day:  2,3,2,0,1
After  2 days: 1,2,1,6,0,8
After  3 days: 0,1,0,5,6,7,8
After  4 days: 6,0,6,4,5,6,7,8,8
After  5 days: 5,6,5,3,4,5,6,7,7,8
After  6 days: 4,5,4,2,3,4,5,6,6,7
After  7 days: 3,4,3,1,2,3,4,5,5,6
After  8 days: 2,3,2,0,1,2,3,4,4,5
After  9 days: 1,2,1,6,0,1,2,3,3,4,8
After 10 days: 0,1,0,5,6,0,1,2,2,3,7,8
After 11 days: 6,0,6,4,5,6,0,1,1,2,6,7,8,8,8
After 12 days: 5,6,5,3,4,5,6,0,0,1,5,6,7,7,7,8,8
After 13 days: 4,5,4,2,3,4,5,6,6,0,4,5,6,6,6,7,7,8,8
After 14 days: 3,4,3,1,2,3,4,5,5,6,3,4,5,5,5,6,6,7,7,8
After 15 days: 2,3,2,0,1,2,3,4,4,5,2,3,4,4,4,5,5,6,6,7
After 16 days: 1,2,1,6,0,1,2,3,3,4,1,2,3,3,3,4,4,5,5,6,8
After 17 days: 0,1,0,5,6,0,1,2,2,3,0,1,2,2,2,3,3,4,4,5,7,8
After 18 days: 6,0,6,4,5,6,0,1,1,2,6,0,1,1,1,2,2,3,3,4,6,7,8,8,8,8

Each day, a 0 becomes a 6 and adds a new 8 to the end of the list, while each other number decreases by 1 if it was present at the start of the day.

In this example, after 18 days, there are a total of 26 fish. After 80 days, there would be a total of 5934.

Find a way to simulate lanternfish.

How many lanternfish would there be after 80 days?

To begin, get your puzzle input.

Part One: Solution

input = Kino.Input.textarea("Please paste your input file:")
fish_list =
  input
  |> Kino.Input.read()
  |> String.split(",", trim: true)
  |> Enum.map(&String.to_integer/1)
[1, 1, 3, 5, 1, 1, 1, 4, 1, 5, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 2, 5, 1, 1, 1, 1, 1, 2, 1, 4, 1,
 4, 1, 1, 1, 1, 1, 3, 1, 1, 5, 1, 1, 1, 4, 1, 1, 1, ...]
defmodule LF do
  defstruct state: [], day: 0

  def increment_days(lf, num_days) do
    for _ <- 0..(num_days - 1), reduce: lf do
      acc -> next_day(acc)
    end
  end

  def next_day(lf) do
    lf
    |> Map.update!(:state, &increment_state/1)
    |> Map.update!(:day, &(&1 + 1))
  end

  defp increment_state(state) do
    state
    |> Enum.map_reduce([], fn
      0, acc -> {6, [8 | acc]}
      fish, acc -> {fish - 1, acc}
    end)
    |> then(fn {old, new} -> old ++ new end)
  end
end
{:module, LF, <<70, 79, 82, 49, 0, 0, 14, ...>>, {:increment_state, 1}}
%LF{state: fish_list}
|> LF.increment_days(80)
|> then(fn x -> length(x.state) end)
396210

Your puzzle answer was 396210.

The first half of this puzzle is complete! It provides one gold star: *

Part Two: Prompt

Suppose the lanternfish live forever and have unlimited food and space. Would they take over the entire ocean?

After 256 days in the example above, there would be a total of 26984457539 lanternfish!

How many lanternfish would there be after 256 days?

Part Two: Solution

This will take too long / too much memory to keep track of the state of all of the fish. Instead, we should keep a count on every day of how many fish we have of each timer type.

fish_list
[1, 1, 3, 5, 1, 1, 1, 4, 1, 5, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 2, 5, 1, 1, 1, 1, 1, 2, 1, 4, 1,
 4, 1, 1, 1, 1, 1, 3, 1, 1, 5, 1, 1, 1, 4, 1, 1, 1, ...]
defmodule LF do
  defstruct count: %{}, start: [], day: 0, size: 0

  def init(state) do
    %__MODULE__{
      start: state,
      count: initial_count(state)
    }
  end

  defp initial_count(list) do
    for x <- list, reduce: %{} do
      acc -> Map.update(acc, x, 1, &(&1 + 1))
    end
  end

  def increment_days(lf, num_days) do
    for _ <- 0..(num_days - 1), reduce: lf do
      acc -> increment_day(acc)
    end
  end

  def increment_day(lf) do
    lf
    |> Map.update!(:count, &increment_count/1)
    |> Map.update!(:day, &(&1 + 1))
    |> update_size()
  end

  defp increment_count(map) do
    Enum.reduce(map, %{}, &adjust_count/2)
  end

  defp adjust_count({0, count}, map) do
    map
    |> Map.update(6, count, &(&1 + count))
    |> Map.update(8, count, &(&1 + count))
  end

  defp adjust_count({k, count}, map) do
    Map.update(map, k - 1, count, &(&1 + count))
  end

  defp update_size(%{count: count} = map) do
    Map.put(map, :size, count |> Map.values() |> Enum.sum())
  end
end
{:module, LF, <<70, 79, 82, 49, 0, 0, 18, ...>>, {:update_size, 1}}
# Check that we still get the same answer as the implementation in Part 1:

fish_list |> LF.init() |> LF.increment_days(80)
%LF{
  count: %{
    0 => 13735,
    1 => 63203,
    2 => 23096,
    3 => 56872,
    4 => 47378,
    5 => 40300,
    6 => 83877,
    7 => 15723,
    8 => 52026
  },
  start: [1, 1, 3, 5, 1, 1, 1, 4, 1, 5, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 2, 5, 1, 1, 1, 1, 1, 2,
   1, 4, 1, 4, 1, 1, 1, 1, 1, 3, 1, 1, 5, 1, 1, 1, 4, 1, ...],
  day: 80,
  size: 396210
}
fish_list |> LF.init() |> LF.increment_days(256)
%LF{
  count: %{
    0 => 146712818362,
    1 => 184289328857,
    2 => 188786353928,
    3 => 202168881355,
    4 => 242460747479,
    5 => 225357049672,
    6 => 298390113008,
    7 => 119908149078,
    8 => 162750099757
  },
  start: [1, 1, 3, 5, 1, 1, 1, 4, 1, 5, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 2, 5, 1, 1, 1, 1, 1, 2,
   1, 4, 1, 4, 1, 1, 1, 1, 1, 3, 1, 1, 5, 1, 1, 1, 4, 1, ...],
  day: 256,
  size: 1770823541496
}

How many lanternfish would there be after 256 days?

Your puzzle answer was 1770823541496.

Both parts of this puzzle are complete! They provide two gold stars: **

At this point, you should return to your Advent calendar and try another puzzle.

If you still want to see it, you can get your puzzle input.