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

Day 16: The Floor Will Be Lava

day16.livemd

Day 16: The Floor Will Be Lava

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

Input

input = Kino.Input.textarea("Please, paste your input here:")
defmodule Day16Shared do
  @default_energies %{up: false, right: false, down: false, left: false}

  def parse(input) do
    input
    |> Kino.Input.read()
    |> String.split("\n")
    |> Enum.with_index()
    |> Enum.reduce(%{}, fn {row, y}, tiles ->
      row
      |> String.split("", trim: true)
      |> Enum.with_index()
      |> Enum.reduce(tiles, fn {tile, x}, tiles ->
        tiles
        |> Map.put({x, y}, tile)
        |> Map.update(:max_x, x, &max(&1, x))
        |> Map.update(:max_y, y, &max(&1, y))
      end)
    end)
  end

  def trace(to_check, tiles_map), do: trace(to_check, tiles_map, %{})

  def trace([], _tiles_map, energized), do: energized

  def trace([{dir, pos} | to_check], tiles_map, energized) do
    next =
      tiles_map
      |> next({dir, pos})
      |> Enum.reject(fn {nxt_dir, nxt_pos} ->
        get_in(energized, [nxt_pos, nxt_dir])
      end)

    new_energized =
      Map.update(energized, pos, @default_energies, fn directions ->
        put_in(directions, [dir], true)
      end)

    trace(next ++ to_check, tiles_map, new_energized)
  end

  def next(tiles_map, {direction, from}) do
    to = till(direction, from)

    case Map.get(tiles_map, to) do
      nil -> []
      "." -> continue(to, direction)
      "-" -> split("-", to, direction)
      "|" -> split("|", to, direction)
      m -> mirror(m, to, direction)
    end
  end

  defp till(:up, {x, y}), do: {x, y - 1}
  defp till(:right, {x, y}), do: {x + 1, y}
  defp till(:down, {x, y}), do: {x, y + 1}
  defp till(:left, {x, y}), do: {x - 1, y}

  defp continue(pos, dir), do: [{dir, pos}]

  defp split("-", pos, dir) when dir in [:right, :left], do: [{dir, pos}]
  defp split("-", pos, :up), do: [{:left, pos}, {:right, pos}]
  defp split("-", pos, :down), do: [{:left, pos}, {:right, pos}]

  defp split("|", pos, dir) when dir in [:up, :down], do: [{dir, pos}]
  defp split("|", pos, :right), do: [{:up, pos}, {:down, pos}]
  defp split("|", pos, :left), do: [{:up, pos}, {:down, pos}]

  defp mirror("/", pos, :up), do: [{:right, pos}]
  defp mirror("/", pos, :right), do: [{:up, pos}]
  defp mirror("/", pos, :down), do: [{:left, pos}]
  defp mirror("/", pos, :left), do: [{:down, pos}]

  defp mirror("\\", pos, :up), do: [{:left, pos}]
  defp mirror("\\", pos, :right), do: [{:down, pos}]
  defp mirror("\\", pos, :down), do: [{:right, pos}]
  defp mirror("\\", pos, :left), do: [{:up, pos}]
end

input
|> Day16Shared.parse()
|> Day16Shared.next({:down, {0, 0}})

Part 1

defmodule Day16Part1 do
  import Day16Shared, except: [parse: 1]

  @start {:right, {-1, 0}}

  def solve(tiles_map) do
    [@start]
    |> trace(tiles_map)
    |> Enum.count()
    |> Kernel.-(1)
  end
end

input
|> Day16Shared.parse()
|> Day16Part1.solve()

# 7472 is the right answer

Part 2

defmodule Day16Part2 do
  import Day16Shared, except: [parse: 1]

  @workers_n System.schedulers_online()

  def solve(%{max_x: mx, max_y: my} = tiles_map) do
    # calculate all the possible starts (points outside the map)
    ups = for x <- 0..mx, y = mx + 1, do: {:up, {x, y}}
    rights = for y <- 0..my, x = -1, do: {:right, {x, y}}
    downs = for x <- 0..mx, y = -1, do: {:down, {x, y}}
    lefts = for y <- 0..my, x = mx + 1, do: {:left, {x, y}}
    starts = ups ++ rights ++ downs ++ lefts

    # calculate all the energized tiles for each start and find maximum
    starts
    |> Enum.chunk_every(@workers_n)
    |> Task.async_stream(
      fn starts ->
        starts
        |> Enum.map(&amp;trace([&amp;1], tiles_map))
        |> Enum.map(&amp;Enum.count/1)
      end,
      max_concurrency: @workers_n
    )
    |> Enum.map(&amp;elem(&amp;1, 1))
    |> List.flatten()
    |> Enum.max()
    |> Kernel.-(1)
  end
end

input
|> Day16Shared.parse()
|> Day16Part2.solve()

# 7716 is the right answer