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

Day 19: Medicine for Rudolph

day_19_medicine_for_rudolph.livemd

Day 19: Medicine for Rudolph

Section

defmodule P1 do
  def parse(input) do
    lines = String.split(input, "\n", trim: true)

    replacement_map =
      Enum.reduce(lines, %{}, fn line, acc ->
        if String.contains?(line, "=>") do
          [from, to] = String.split(line, " => ")
          Map.update(acc, from, [to], &[to | &1])
        else
          acc
        end
      end)

    medicine_string = List.last(lines)
    {replacement_map, medicine_string}
  end

  def replace_all(medicine_string, replacement_map) do
    Enum.reduce(replacement_map, MapSet.new(), fn {from, tos}, acc ->
      Enum.reduce(tos, acc, fn to, acc_inner ->
        apply_replacements(medicine_string, from, to, acc_inner)
      end)
    end)
  end

  defp apply_replacements(medicine_string, from, to, acc) do
    Regex.scan(~r/#{from}/, medicine_string, return: :index)
    |> Enum.reduce(acc, fn [{start, length}], acc_inner ->
      {prefix, rest} = String.split_at(medicine_string, start)
      {_match, suffix} = String.split_at(rest, length)
      new_medicine = prefix <> to <> suffix
      MapSet.put(acc_inner, new_medicine)
    end)
  end

  def expand_to_target(replacement_map, target) do
    bfs([{"e", 0}], replacement_map, target, MapSet.new(["e"]))
  end

  # If no solution is found
  defp bfs([], _replacement_map, _target, _visited), do: -1

  defp bfs([{current, steps} | rest], replacement_map, target, visited) do
    if current == target do
      steps
    else
      next_states = generate_next_states(current, replacement_map, visited, steps)
      bfs(rest ++ next_states, replacement_map, target, visited)
    end
  end

  defp generate_next_states(molecule, replacement_map, visited, steps) do
    Enum.reduce(replacement_map, [], fn {from, tos}, acc ->
      Enum.reduce(tos, acc, fn to, acc_inner ->
        apply_replacements(molecule, from, to, visited, steps, acc_inner)
      end)
    end)
  end

  defp apply_replacements(molecule, from, to, visited, steps, acc) do
    Regex.scan(~r/#{from}/, molecule, return: :index)
    |> Enum.reduce(acc, fn [{start, length}], acc_inner ->
      {prefix, rest} = String.split_at(molecule, start)
      {_match, suffix} = String.split_at(rest, length)
      new_molecule = prefix <> to <> suffix

      if MapSet.member?(visited, new_molecule) do
        acc_inner
      else
        [{new_molecule, steps + 1} | acc_inner]
      end
    end)
  end
end

{map, target} =
  "/Users/eli/Desktop/input.txt"
  |> File.read!()
  |> P1.parse()

P1.replace_all(target, map)
|> Enum.count()
576
defmodule P2 do
  def create_reverse_map(replacement_map) do
    Enum.reduce(replacement_map, %{}, fn {from, tos}, acc ->
      Enum.reduce(tos, acc, fn to, acc_inner ->
        Map.update(acc_inner, to, [from], &amp;[from | &amp;1])
      end)
    end)
  end

  def reduce_to_e(target, reverse_map) do
    dfs(target, reverse_map, 0, %MapSet{})
  end

  defp dfs("e", _reverse_map, steps, _visited) do
    {:ok, steps}
  end

  defp dfs(target, reverse_map, steps, visited) do
    if MapSet.member?(visited, target) do
      {:error, :cycle_detected}
    else
      visited = MapSet.put(visited, target)

      case reduce_xrnyar(target, reverse_map, steps, visited) do
        {:ok, new_target, new_steps} -> dfs(new_target, reverse_map, new_steps, visited)
        :error -> reduce_greedily(target, reverse_map, steps, visited)
      end
    end
  end

  defp reduce_xrnyar(target, reverse_map, steps, visited) do
    regex = ~r/(\w+)(Rn)(\w+)(Ar)/

    case Regex.run(regex, target, capture: :all_but_first) do
      [_x, "Rn", y, "Ar"] ->
        reduced_y =
          Enum.reduce_while(reverse_map, y, fn {from, [to | _]}, acc ->
            if String.contains?(acc, from) do
              {:halt, String.replace(acc, from, to, global: false)}
            else
              {:cont, acc}
            end
          end)

        if reduced_y == y do
          :error
        else
          new_target = String.replace(target, y, reduced_y)

          case reduce_greedily(new_target, reverse_map, steps + 1, visited) do
            {:ok, final_target, final_steps} -> {:ok, final_target, final_steps}
            _ -> :error
          end
        end

      _ ->
        :error
    end
  end

  defp reduce_greedily(target, reverse_map, steps, visited) do
    Enum.find_value(reverse_map, {:error, :no_reduction}, fn {from, [to | _]} ->
      if String.contains?(target, from) do
        new_target = String.replace(target, from, to, global: false)

        case dfs(new_target, reverse_map, steps + 1, visited) do
          {:ok, _} = result -> result
          _ -> nil
        end
      else
        nil
      end
    end)
  end
end

reverse_map = P2.create_reverse_map(map)
P2.reduce_to_e(target, reverse_map)
{:ok, 207}