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

Day 19

day19.livemd

Day 19

Mix.install([
  {:kino, "~> 0.14.2"}
])

IEx.Helpers.c("/Users/johnb/dev/2015adventOfCode/advent_of_code.ex")
alias AdventOfCode, as: AOC
alias Kino.Input
require Integer

# Note: when making the next template, something like this works well:
#   `cat day01.livemd | sed 's/01/02/' > day02.livemd`
#

Installation and Data

input_p1example = Kino.Input.textarea("Example Data")
input_p1puzzleInput = Kino.Input.textarea("Puzzle Input")
input_source_select =
  Kino.Input.select("Source", [{:example, "example"}, {:puzzle_input, "puzzle input"}])
p1data = fn ->
  (Kino.Input.read(input_source_select) == :example &&
     Kino.Input.read(input_p1example)) ||
    Kino.Input.read(input_p1puzzleInput)
end

Solution

defmodule Day19 do
  @replacement_parser ~r/(\w+) => (\w+)/

  def parse_replacements(text) do
    text
    |> AOC.as_single_lines()
    |> Enum.reduce( %{}, fn line, acc ->
      [[_, start, replacement]] = Regex.scan(@replacement_parser, line)
      
      {_, acc} = get_and_update_in(acc[start], fn current ->
        {current, (current || []) ++ [replacement]}
      end)
  
      acc
    end)
  end

  def enumerate_new_molecules(medicine, replacements) do
    replacements
    |> Enum.reduce(%{}, fn {source, dests}, acc ->
      splits = String.split(medicine, source)
      
      dests
      |> Enum.reduce(acc, fn dest, acc1 ->
        [_left | rest] = Enum.with_index(splits)

        rest
        |> Enum.reduce(acc1, fn {_split, index}, acc2 ->
          prefix = Enum.slice(splits, 0..(index - 1)//1)
            |> Enum.join(source)
          center = dest # only replace one section at a time
          ending = Enum.slice(splits, (index)..-1//1)
            |> Enum.join(source)
          new_molecule = [prefix, center, ending]
            |> Enum.join("")
          
          {_, acc3} = get_and_update_in(acc2[new_molecule], fn current -> 
            {current, (current || 0) + 1} 
          end)
          
          acc3
        end)
      end)
    end)
  end
  
  def solve1(text) do
    [replacements, medicine] = AOC.as_doublespaced_paragraphs(text)
    replacements = parse_replacements(replacements)
    molecules = enumerate_new_molecules(medicine, replacements)

    molecules
    |> Map.keys()
    |> Enum.count()
  end

  # %{key => [val1, val2]}
  #   becomes
  # %{val1 => key, val2 => key}
  def reverse_replacements(replacements) do
    Enum.reduce(replacements, %{}, fn {key, values}, acc ->
      Enum.reduce(values, acc, fn value, acc1 ->
        put_in(acc1, [value], key)
      end)
    end)
  end
  
  def solve2(text) do
    [replacements, medicine] = AOC.as_doublespaced_paragraphs(text)
    replacements = parse_replacements(replacements)

    reversals = reverse_replacements(replacements)
      |> Enum.sort_by(fn {key, _value} -> String.length(key) end, :desc)
      |> Enum.map(fn {k, v} -> [k,v] end)

    # Iterate thru all the replacements possible, starting at the longest.
    Enum.reduce(1..300, {0, medicine}, fn _iteration, {replace_count, new_medicine} ->
      reversals
      |> Enum.reduce({replace_count, new_medicine}, fn [src, dest], {rep_count2, new_med2} ->
        med = String.replace(new_med2, src, dest, global: false)
        if med == new_med2 do
          {rep_count2, new_med2}
        else
          {rep_count2 + 1, med}
        end
      end)
    end)
  end
end

# Example data:
# e => H
# e => O
# H => HO
# H => OH
# O => HH
#
# HOHOHO

p1data.()
|> Day19.solve1()
|> IO.inspect(label: "\n*** Part 1 solution (example: 7 for HOHOHO)")
# 535

p1data.()
|> Day19.solve2()
|> IO.inspect(label: "\n*** Part 2 solution (example: 6)")
# 212