Powered by AppSignal & Oban Pro

AoC 2022 Day 16

2022/day16.livemd

AoC 2022 Day 16

Mix.install([:kino, :libgraph])

defmodule Utils do
  def split(line, sep \\ "") do
    String.split(line, sep, trim: true)
  end

  def split_all_lines(text, sep \\ "") do
    text
    |> String.split("\n", trim: true)
    |> Enum.map(&split(&1, sep))
  end

  def to_numbers(number) when is_binary(number) do
    String.to_integer(number)
  end

  def to_numbers(numbers) when is_list(numbers) do
    Enum.map(numbers, &to_numbers/1)
  end

  def to_matrix(text, sep \\ "") do
    text
    |> split_all_lines(sep)
    |> then(fn data ->
      for {row, r} <- Enum.with_index(data), {col, c} <- Enum.with_index(row) do
        {{r, c}, col}
      end
    end)
    |> Map.new()
  end
end

Setup

import Utils
input = Kino.Input.textarea("Input:")
text = Kino.Input.read(input)
data =
  split(text, "\n")
  |> Map.new(fn line ->
    [from | to] =
      ~r/[A-Z]{2}/
      |> Regex.scan(line)
      |> List.flatten()

    rate =
      ~r/\d+/
      |> Regex.scan(line)
      |> List.flatten()
      |> to_numbers()
      |> List.first()

    {from, %{neighbours: to, rate: rate}}
  end)

P1

defmodule P1 do
  def solve(data, ticks \\ 30) do
    graph =
      Graph.add_edges(
        Graph.new(),
        data
        |> Enum.map(fn {k, v} -> Enum.map(v.neighbours, &amp;{k, &amp;1}) end)
        |> List.flatten()
      )

    rates = Map.new(data, fn {k, v} -> {k, v.rate} end)

    candidates =
      data |> Enum.filter(fn {_k, v} -> v.rate > 0 end) |> Enum.map(fn {k, _v} -> k end)

    cache =
      (for c <- candidates do
         {{"AA", c}, Graph.get_shortest_path(graph, "AA", c) |> length |> Kernel.-(1)}
       end ++
         for c1 <- candidates, c2 <- candidates, c1 != c2 do
           {{c1, c2}, Graph.get_shortest_path(graph, c1, c2) |> length |> Kernel.-(1)}
         end)
      |> Map.new()

    search(rates, "AA", candidates, ticks, cache)
  end

  def search(rates, curr, candidates, remaining, cache) do
    candidates
    |> Enum.map(fn next ->
      cost = cache[{curr, next}]
      remaining = remaining - cost - 1
      gain = rates[next] * remaining

      if remaining > 0 do
        gain + search(rates, next, candidates -- [next], remaining, cache)
      else
        0
      end
    end)
    |> Enum.max(fn -> 0 end)
  end
end
P1.solve(data)

P2

defmodule P2 do
  def solve(data, ticks \\ 26) do
    graph =
      Graph.add_edges(
        Graph.new(),
        data
        |> Enum.map(fn {k, v} -> Enum.map(v.neighbours, &amp;{k, &amp;1}) end)
        |> List.flatten()
      )

    rates = Map.new(data, fn {k, v} -> {k, v.rate} end)

    candidates =
      data |> Enum.filter(fn {_k, v} -> v.rate > 0 end) |> Enum.map(fn {k, _v} -> k end)

    cache =
      (for c <- candidates do
         {{"AA", c}, Graph.get_shortest_path(graph, "AA", c) |> length |> Kernel.-(1)}
       end ++
         for c1 <- candidates, c2 <- candidates, c1 != c2 do
           {{c1, c2}, Graph.get_shortest_path(graph, c1, c2) |> length |> Kernel.-(1)}
         end)
      |> Map.new()

    search(rates, [{ticks, "AA"}, {ticks, "AA"}], candidates, cache)
  end

  def search(rates, state, candidates, cache) do
    {{min, standby}, {max, curr}} = Enum.min_max(state)

    candidates
    |> Enum.map(fn next ->
      cost = cache[{curr, next}]
      remaining = max - cost - 1
      gain = rates[next] * remaining

      if remaining > 0 do
        gain + search(rates, [{min, standby}, {remaining, next}], candidates -- [next], cache)
      else
        if min > 0 do
          P1.search(rates, standby, candidates, min, cache)
        else
          0
        end
      end
    end)
    |> Enum.max(fn -> 0 end)
  end
end
P2.solve(data)