Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Advent 2021 - Day 14

day14.livemd

Advent 2021 - Day 14

Setup

Mix.install([
  {:kino, github: "livebook-dev/kino"}
])
input = Kino.Input.textarea("Please paste your input file:")
[input, rules] =
  input
  |> Kino.Input.read()
  |> String.trim()
  |> String.split("\n\n")

input = String.graphemes(input)

rules =
  rules
  |> String.split("\n")
  |> Enum.map(fn rule ->
    [pair, result] = rule |> String.split(" -> ")
    pair = pair |> String.graphemes() |> List.to_tuple()
    {pair, result}
  end)
  |> Map.new()

{input, rules}

Part 1

Using the naive approach of actually creating a linked list

defmodule Part1 do
  def step(input, rules) do
    partial =
      Enum.chunk_every(input, 2, 1, :discard)
      |> Enum.flat_map(fn [left, right] -> [left, Map.get(rules, {left, right})] end)

    partial ++ [List.last(input)]
  end

  def run(input, rules, times) do
    Enum.reduce(1..times, input, fn _, input -> step(input, rules) end)
  end
end

result =
  Part1.run(input, rules, 10)
  |> Enum.group_by(& &1)
  |> Map.values()
  |> Enum.map(&Enum.count(&1))

Enum.max(result) - Enum.min(result)

Part 2

Using the method of counting pairs!

It may seem like you have to keep track of the entire string to know how to go from one state to another, but think about it this way.

If you have CH -> B, it will result in the string CBH, but the string CBH is actually pairs CB and BH.

Using this logic, you lose pair CH, but gain pairs CB and BH!

Therefore if you had 5 pairs of CH, in a given step, you’d gain 5 pairs of CB and BH, and lose 5 pairs of CH, but then gain how ever many CH was produced from other pairs that would produce CH.

defmodule Part2 do
  def step(input, rules) do
    Enum.reduce(input, input, fn {{left, right}, amount}, acc ->
      middle = Map.get(rules, {left, right})

      inc = fn existing ->
        if existing == nil do
          {existing, amount}
        else
          {existing, existing + amount}
        end
      end

      dec = fn existing ->
        if existing == nil do
          {existing, amount}
        else
          {existing, existing - amount}
        end
      end

      acc
      |> Map.get_and_update({left, middle}, inc)
      |> elem(1)
      |> Map.get_and_update({middle, right}, inc)
      |> elem(1)
      |> Map.get_and_update({left, right}, dec)
      |> elem(1)
    end)
  end

  def run(input, rules, times) do
    Enum.reduce(1..times, input, fn _, input ->
      step(input, rules)
    end)
  end
end

pair_count =
  input
  |> Enum.chunk_every(2, 1, :discard)
  |> Enum.map(&List.to_tuple(&1))
  |> Enum.group_by(& &1)
  |> Map.map(fn {_, v} -> Enum.count(v) end)
  |> Part2.run(rules, 40)

result =
  pair_count
  |> Enum.flat_map(fn {{left, right}, amount} -> [{left, amount}, {right, amount}] end)
  |> Enum.group_by(fn {chemical, _} -> chemical end, fn {_, amount} -> amount end)
  |> Map.map(fn {_, v} -> ceil(Enum.sum(v) / 2) end)
  |> Map.values()

Enum.max(result) - Enum.min(result)