Powered by AppSignal & Oban Pro

🎄 Year 2022 🔔 Day 11

elixir/notebooks/2022/day11.livemd

🎄 Year 2022 🔔 Day 11

Problem

Parse input

filename = "#{__DIR__}/../../../inputs/2022/day11.txt"

defmodule Parser do
  def parse_line([monkey_id, starting_items, operation, test, if_true, if_false]) do
    {
      parse_monkey_id(monkey_id),
      %{
        items: parse_starting_items(starting_items),
        operation: parse_operation(operation),
        test: parse_test(test),
        if_true: parse_if_true(if_true),
        if_false: parse_if_false(if_false),
        inspections: 0
      }
    }
  end

  defp parse_monkey_id("Monkey " <> monkey_id), do: String.to_integer(monkey_id)

  defp parse_starting_items("Starting items: " <> starting_items),
    do: String.split(starting_items, ", ") |> Enum.map(&String.to_integer/1)

  defp parse_operation("Operation: new = " <> operation) do
    String.split(operation, " ")
    |> Enum.map(fn e ->
      case Integer.parse(e) do
        {n, ""} -> n
        :error -> e
      end
    end)
  end

  defp parse_test("Test: divisible by " <> n), do: String.to_integer(n)
  defp parse_if_false("If false: throw to monkey " <> n), do: String.to_integer(n)
  defp parse_if_true("If true: throw to monkey " <> n), do: String.to_integer(n)
end

starting_monkeys =
  File.read!(filename)
  |> String.split("\n\n")
  |> Enum.map(&String.split(&1, "\n"))
  |> Enum.map(fn list ->
    Enum.map(list, fn line ->
      String.trim(line, " ") |> String.trim(",") |> String.trim(":")
    end)
  end)
  |> Enum.map(&Enum.reject(&1, fn e -> e == "" end))
  |> Enum.map(&Parser.parse_line/1)
  |> Map.new()

monkey_count = Enum.count(starting_monkeys)

Shared code

defmodule MonkeyBusiness do
  def operate(["old", "+", "old"], item), do: item + item
  def operate(["old", "+", n], item), do: item + n
  def operate(["old", "*", "old"], item), do: item * item
  def operate(["old", "*", n], item), do: item * n

  def perform_round(monkeys, worry_divider, keep_worry_remainder) do
    monkey_count = Enum.count(monkeys)

    0..(monkey_count - 1)
    |> Enum.reduce(monkeys, fn monkey_id, monkeys ->
      %{
        items: items,
        operation: operation,
        test: test,
        if_true: if_true,
        if_false: if_false,
        inspections: inspections
      } = Map.fetch!(monkeys, monkey_id)

      new_inspections = inspections + Enum.count(items)

      %{true: true_items, false: false_items} =
        items
        |> Enum.map(&MonkeyBusiness.operate(operation, &1))
        |> Enum.map(fn e ->
          if keep_worry_remainder do
            rem(e, worry_divider)
          else
            div(e, worry_divider)
          end
        end)
        |> Enum.group_by(fn e -> rem(e, test) == 0 end)
        |> then(&Map.merge(%{true: [], false: []}, &1))

      monkeys
      |> Map.update!(monkey_id, &Map.update!(&1, :items, fn _items -> [] end))
      |> Map.update!(monkey_id, &Map.update!(&1, :inspections, fn _ -> new_inspections end))
      |> Map.update!(
        if_true,
        &Map.update!(&1, :items, fn items -> Enum.concat(items, true_items) end)
      )
      |> Map.update!(
        if_false,
        &Map.update!(&1, :items, fn items -> Enum.concat(items, false_items) end)
      )
    end)
  end
end

Part One

Stream.repeatedly(fn -> nil end)
|> Enum.take(20)
|> Enum.reduce(starting_monkeys, fn nil, monkeys ->
  MonkeyBusiness.perform_round(monkeys, 3, false)
end)
|> Enum.map(fn {monkey_id, monkey_map} -> Map.fetch!(monkey_map, :inspections) end)
|> Enum.sort(:desc)
|> Enum.take(2)
|> Enum.product()

Part two

worry_divider =
  starting_monkeys
  |> Enum.map(fn {_, %{test: test}} -> test end)
  |> Enum.uniq()
  |> Enum.product()

Stream.repeatedly(fn -> nil end)
|> Enum.take(10000)
|> Enum.reduce(starting_monkeys, fn nil, monkeys ->
  MonkeyBusiness.perform_round(monkeys, worry_divider, true)
end)
|> Enum.map(fn {monkey_id, monkey_map} -> Map.fetch!(monkey_map, :inspections) end)
|> Enum.sort(:desc)
|> Enum.take(2)
|> Enum.product()