Powered by AppSignal & Oban Pro

d16

d16/d16.livemd

d16

Section

defmodule D16 do
  def parse(input) do
    for {line, row} <- Enum.with_index(String.split(input)),
        {char, col} <- Enum.with_index(String.to_charlist(line)),
        reduce: {MapSet.new(), nil, nil} do
      {walls, start, finish} ->
        case char do
          ?# -> {MapSet.put(walls, {row, col}), start, finish}
          ?S -> {walls, {row, col}, finish}
          ?E -> {walls, start, {row, col}}
          ?. -> {walls, start, finish}
        end
    end
  end

  defp vec_add({r, c}, {dr, dc}), do: {r + dr, c + dc}
  defp rot_cw({dr, dc}), do: {dc, -dr}
  defp rot_ccw({dr, dc}), do: {-dc, dr}

  defp pop_min(distance, frontier) do
    key = Enum.min_by(frontier, &Map.get(distance, &1))
    {key, MapSet.delete(frontier, key)}
  end

  defp dijkstra(walls, distance, frontier, finish) do
    {key, frontier} = pop_min(distance, frontier)
    {pos, heading} = key

    cond do
      key == finish ->
        distance

      is_nil(heading) ->
        dijkstra(walls, distance, frontier, finish)

      true ->
        {distance, frontier} =
          for {dest = {dpos, _}, cost} <- [
                {{vec_add(pos, heading), heading}, 1},
                {{pos, rot_cw(heading)}, 1000},
                {{pos, rot_ccw(heading)}, 1000},
                {{pos, nil}, 0}
              ],
              reduce: {distance, frontier} do
            {distance, frontier} ->
              new_distance = Map.get(distance, key) + cost
              old_distance = Map.get(distance, dest)

              if !MapSet.member?(walls, dpos) and
                   (is_nil(old_distance) or
                      new_distance < old_distance) do
                {Map.put(distance, dest, new_distance), MapSet.put(frontier, dest)}
              else
                {distance, frontier}
              end
          end

        dijkstra(walls, distance, frontier, finish)
    end
  end

  def part1({walls, start, finish}) do
    key = {start, {0, 1}}
    distance = dijkstra(walls, %{key => 0}, MapSet.new([key]), {finish, nil})
    Map.get(distance, {finish, nil})
  end

  defp reconstruct(_, _, _, visited, []), do: visited

  defp reconstruct(walls, distance, start, visited, [head | heads]) when head == start do
    reconstruct(walls, distance, start, visited, heads)
  end

  defp reconstruct(walls, distance, start, visited, [key = {pos, heading} | heads]) do
    cost = Map.get(distance, key)

    neighbors =
      if is_nil(heading) do
        [{-1, 0}, {0, 1}, {1, 0}, {0, -1}]
        |> Enum.map(&{{pos, &1}, 0})
      else
        [
          {{vec_add(pos, rot_cw(rot_cw(heading))), heading}, 1},
          {{pos, rot_cw(heading)}, 1000},
          {{pos, rot_ccw(heading)}, 1000}
        ]
      end
      |> Enum.filter(fn {key, points} ->
          Map.get(distance, key) == cost - points
      end)
      |> Enum.map(&elem(&1, 0))

    reconstruct(walls, distance, start, MapSet.put(visited, key), neighbors ++ heads)
  end

  def part2({walls, start, finish}) do
    key = {start, {0, 1}}
    distance = dijkstra(walls, %{key => 0}, MapSet.new([key]), {finish, nil})

    reconstruct(walls, distance, key, MapSet.new(), [{finish, nil}])
    |> Enum.map(&elem(&1, 0))
    |> Enum.uniq()
    |> Enum.count()
  end
end
ExUnit.start()

defmodule D16.Test do
  use ExUnit.Case

  test "small" do
    input = "###############
#.......#....E#
#.#.###.#.###.#
#.....#.#...#.#
#.###.#####.#.#
#.#.#.......#.#
#.#.#####.###.#
#...........#.#
###.#.#####.#.#
#...#.....#.#.#
#.#.#.###.#.#.#
#.....#...#.#.#
#.###.#.#.#.#.#
#S..#.....#...#
###############
"
    assert 7036 == D16.parse(input) |> D16.part1()
    assert 45 == D16.parse(input) |> D16.part2()
  end

  test "large" do
    input = "#################
#...#...#...#..E#
#.#.#.#.#.#.#.#.#
#.#.#.#...#...#.#
#.#.#.#.###.#.#.#
#...#.#.#.....#.#
#.#.#.#.#.#####.#
#.#...#.#.#.....#
#.#.#####.#.###.#
#.#.#.......#...#
#.#.###.#####.###
#.#.#...#.....#.#
#.#.#.#####.###.#
#.#.#.........#.#
#.#.#.#########.#
#S#.............#
#################
"
    assert 11048 == D16.parse(input) |> D16.part1()
    assert 64 == D16.parse(input) |> D16.part2()
  end
end

ExUnit.run()
input = File.read!(__DIR__ <> "/input")
D16.parse(input) |> D16.part1()
D16.parse(input) |> D16.part2()