Powered by AppSignal & Oban Pro

--- Day 14: Extended Polymerization ---

2021/day14.livemd

— Day 14: Extended Polymerization —

Section

defmodule Setup do
  def get_input(prompt) do
    case IO.gets(prompt) do
      :eof -> ""
      line -> line <> get_input(prompt)
    end
  end

  def parse(binary) do
    binary
    |> String.split("\n\n", trim: true)
    |> then(fn [template, rules] ->
      {String.to_charlist(template),
       rules
       |> String.split([" -> ", "\n"], trim: true)
       |> Enum.chunk_every(2)
       |> Map.new(fn [<<a, b>>, <<c>>] -> {[a, b], c} end)}
    end)
  end
end

{template, rules} =
  Setup.get_input("input")
  |> Setup.parse()
defmodule Poly do
  def grow(pair_counts, rules) do
    # given the pair count of the previous template we calculate the new pair counts
    #   according to the given polymere grow rules
    Enum.reduce(pair_counts, %{}, fn {[a, b] = pair, count}, pair_counts ->
      # each pair generates two new pairs, discarding itself:
      #    AB -> C generates AC and CB for every AB-pair in the template
      #
      # NOTE: different pairs in the template can generate 
      #    the same pairs in the next growth step
      pair_counts
      |> Map.update([a, rules[pair]], count, &(&1 + count))
      |> Map.update([rules[pair], b], count, &(&1 + count))
    end)
  end

  defp pairs(template) do
    # given a template generate all pairs in that template
    # ABCD -> [AB, BC, CD]
    template |> Enum.chunk_every(2, 1, :discard) |> Enum.frequencies()
  end

  def solve(template, rules, steps) do
    Enum.reduce(1..steps, pairs(template), fn _step, acc -> grow(acc, rules) end)
    # It returns the pair count after N steps
    #
    # Now we do some counting on the elements
    |> Enum.reduce(%{}, fn {[a, b], count}, acc ->
      # Create a map with counts for each element
      # Each pair adds 2*N elements to the list where N is the number 
      #    of occurances of the pair.
      acc
      |> Map.update(a, count, &(&1 + count))
      |> Map.update(b, count, &(&1 + count))
    end)
    # All elements are counted twice as they appear in two pairs
    # AB BC CD ---> 2xB, 2xC, but the edges are counted once
    # we correct that here:
    |> Map.update!(List.first(template), &(&1 + 1))
    |> Map.update!(List.last(template), &(&1 + 1))
    # we're not interested in the letters anymore
    |> Map.values()
    # we only want to know the min/max values
    |> Enum.min_max()
    |> then(fn {min, max} ->
      # substract, and correct for the double counting
      div(max - min, 2)
    end)
  end
end

Part1

Poly.solve(template, rules, 10)

Part2

Poly.solve(template, rules, 40)