Advent of Code 2021 - Day 6

This topic is about Day 6 of the Advent of Code 2021.

We have a private leaderboard (shared with users of Erlang Forums ):

https://adventofcode.com/2021/leaderboard/private/view/370884

The entry code is:
370884-a6a71927

1 Like

Here is my solution:

1 Like

I went with representing the frequencies as a tuple and a simulation step was:

defp simulate({s0, s1, s2, s3, s4, s5, s6, s7, s8}, days) when days > 0 do
  simulate({s1, s2, s3, s4, s5, s6, s7 + s0, s8, s0}, days - 1)
end

Full solution

It’d be nice to see an Nx solution here, it fits this problem very nicely.

7 Likes
1 Like

I did it the “slow” way the first time and got instant gratification, the later part, not so instant (in fact not all) until I remembered bucket sort :slight_smile: and my good old friends Enum.frequencies and Map.merge/3

Here’s how the end product looked like:

defmodule AdventOfCode.Y2021.Day06 do
  use AdventOfCode.Helpers.InputReader, year: 2021, day: 6

  def run_1, do: input!() |> parse() |> multiply(80) |> Enum.sum()
  def run_2, do: input!() |> parse() |> multiply(256) |> Enum.sum()
  def parse(f), do: f |> String.split(",") |> Enum.map(&String.to_integer/1) |> Enum.frequencies()

  def multiply(fishes, day) do
    (day == 0 && Map.values(fishes)) ||
      multiply(
        Map.pop(fishes, 0)
        |> then(
          &Map.merge(
            for({k, v} <- elem(&1, 1), into: %{}, do: {k - 1, v}),
            %{6 => elem(&1, 0) || 0, 8 => elem(&1, 0) || 0},
            fn _, a, b -> a + b end
          )
        ),
        day - 1
      )
  end
end
1 Like

struggling with the size of part 2. I want to do this in a mathematical way following an exponential growth equation like number of fishes * (1 + rate of growth)^number of days but this doesn’t work.

2 Likes

I enjoyed this one! :smiley:

defmodule Aoc.Y2021.Day06 do
  @moduledoc """
  Solved https://adventofcode.com/2021/day/6
  """
  import Aoc.Helper.IO

  def run_part1(), do: get_input() |> solve_part1()
  def run_part2(), do: get_input() |> solve_part2()

  def solve_part1(data), do: solve(data, 80)
  def solve_part2(data), do: solve(data, 256)

  def solve(data, days), do: data |> fish_count() |> simulate(days) |> Enum.sum()
  def fish_count(data), do: Enum.map(0..8, fn n -> Map.get(Enum.frequencies(data), n, 0) end)

  def simulate(fish_count, 0), do: fish_count

  def simulate([zeroth, first, second, third, fourth, fifth, sixth, seventh, eighth], days) do
    simulate([first, second, third, fourth, fifth, sixth, seventh + zeroth, eighth, zeroth], days - 1)
  end

  defp get_input(), do: get_integer_input("2021", "06", ",")
end


6 Likes

I wish I analyzed input properties more before jumping to solving things. The same happened with Bingo, the solution looked different in my head before and after I realized it’s a 5x5 grid all the time.

Looking forward to what you come up with. I was thinking the same thing but got lazy (scared) and backed away.

I’m going to have to go to bed without solving it. My initial idea was Bn= B0 x 2^Kt where B0 is the original count for each timer value. K is 1/7 and t (for part 2) is 256 - the original timer value. Just calculate that for each original timer value and then add them together. Not sure how to make it work though.

DERP.
Not accounting for the maturation time with each generation.

1 Like

I solved Part I with a DynamicSupervisor and a process per a fish approach :slight_smile:

I expected fish behavioral features are to be changed in Part II and boom.

2 Likes

Really nice!

1 Like

It’s nice when it works on part 2 without modifications. Not the most clever solution but it’s fast enough!

defmodule AdventOfCode.Day06 do
  def part1(input) do
    input
    |> parse_input()
    |> Enum.frequencies()
    |> pass_days(80)
    |> Map.values()
    |> Enum.sum()
  end

  def part2(input) do
    input
    |> parse_input()
    |> Enum.frequencies()
    |> pass_days(256)
    |> Map.values()
    |> Enum.sum()
  end

  def pass_days(fish, 0), do: fish

  def pass_days(fish, days_left) do
    fish
    |> Enum.reduce(%{}, fn
      {0, qt}, acc ->
        acc |> Map.update(6, qt, &(&1 + qt)) |> Map.put(8, qt)

      {n, qt}, acc ->
        acc |> Map.update(n - 1, qt, &(&1 + qt))
    end)
    |> pass_days(days_left - 1)
  end

  def parse_input(input) do
    input |> String.trim() |> String.split(",") |> Enum.map(&String.to_integer/1)
  end
end
1 Like

My solution:
I used 2 maps to track two groups of fish. fish cycles 7 days, spawn cycles 9 days.
I couldn’t figure a way to track of all fish at the same rate, so I split into two groups, each with a different “gestation” rate.

Runs ~250ms on Apple M1.

defmodule Advent.Y2021.D06 do
  @spec part_one([integer()], integer()) :: integer()
  def part_one(seed, days) do
    map_solution(seed, days)
  end

  @spec part_two([integer()], integer()) :: integer()
  def part_two(seed, days) do
    map_solution(seed, days)
  end

  defp map_solution(seed, days) do
    fish = Enum.frequencies(seed)

    Stream.unfold({0, fish, %{}}, fn
      {day, fish, spawn} ->
        fish_cycle = rem(day, 7)
        spawn_cycle = rem(day, 9)

        num_new_fish = Map.get(spawn, spawn_cycle, 0)
        fish = Map.update(fish, fish_cycle, num_new_fish, &(&1 + num_new_fish))

        spawn = Map.put(spawn, spawn_cycle, Map.fetch!(fish, fish_cycle))

        {{fish, spawn}, {day + 1, fish, spawn}}
    end)
    |> Enum.at(days - 1)
    |> (fn {fish, spawn} ->
          Map.merge(fish, spawn, fn _k, v1, v2 -> v1 + v2 end)
        end).()
    |> Map.values()
    |> Enum.sum()
  end
end

input =
  Path.join(["d06_input.txt"])
  |> File.stream!()
  |> Stream.map(&String.trim/1)
  |> Enum.at(0)
  |> String.split(",")
  |> Enum.map(&String.to_integer/1)

IO.inspect(Advent.Y2021.D06.part_one(input, 80))
IO.inspect(Advent.Y2021.D06.part_two(input, 256))

I originally had a function list_solution/2 I used for part one. It took way too long for part two (I killed attempt before completion). But here it is incase there’s any interest

@spec list_solution([integer()], integer()) :: integer()
defp list_solution(seed, days) do
  Stream.iterate(seed, fn fish ->
    num_spawning_fish = Enum.count(fish, &(&1 == 0))

    fish =
      Enum.map(fish, fn
        0 -> 6
        n -> n - 1
      end)

    fish ++ List.duplicate(8, num_spawning_fish)
  end)
  |> Enum.at(days)
  |> length()
end

Looking at others, I see that updating the map key isn’t expensive, and performance is similar - probably the cleaner/nicer way to go!

My solution with Livebook: y2021/day-06.livemd

My solution
Using frequencies as everybody else :slight_smile:

Eh, one of the more difficult days for me.
It was immediately clear to me that recursion with big lists is not the way to solve this.
But I was not able to realize that Enum.frequencies is the key to this. Quite frustrating, once again. :sweat_smile:
I had to take a peek here. Once I saw it mentioned, it was so obvious.
day_06.livemd

1 Like

Mine was very similar to some others here

fish
|> Enum.reduce(%{}, fn {timer, count}, acc ->
  if timer > 0 do
    tally_fish(acc, timer - 1, count)
  else
    acc
    |> tally_fish(6, count)
    |> tally_fish(8, count)
  end
end)

Full solution here

1 Like