defmodule Day16 do defmodule State do @enforce_keys [:pqueue, :map, :path, :dist] defstruct [:pqueue, :map, :path, :dist] def new(game) do pqueue = :gb_sets.from_list([{0, game.start}]) map = :gb_trees.insert(game.start, 0, :gb_trees.empty()) path = Map.new([{game.start, []}]) dist = Map.new([{game.start, 0}]) %State{pqueue: pqueue, map: map, path: path, dist: dist} end def upsert(state, posdir, dist, prev) do case :gb_trees.lookup(posdir, state.map) do :none -> pqueue = :gb_sets.insert({dist, posdir}, state.pqueue) map = :gb_trees.insert(posdir, dist, state.map) path = Map.put(state.path, posdir, [prev]) distmap = Map.update(state.dist, posdir, dist, &min(&1, dist)) %State{pqueue: pqueue, map: map, path: path, dist: distmap} {_, odist} -> distmap = Map.update!(state.dist, posdir, &min(&1, dist)) cond do dist > odist -> %State{state | dist: distmap} dist == odist -> path = Map.update!(state.path, posdir, &[prev | &1]) %State{state | path: path, dist: distmap} true -> pqueue = :gb_sets.delete({odist, posdir}, state.pqueue) pqueue = :gb_sets.insert({dist, posdir}, pqueue) map = :gb_trees.update(posdir, dist, state.map) path = Map.put(state.path, posdir, [prev]) %State{pqueue: pqueue, map: map, path: path, dist: distmap} end end end def empty?(state) do :gb_sets.is_empty(state.pqueue) end def pop(state) do {v, pqueue} = :gb_sets.take_smallest(state.pqueue) {v, %State{state | pqueue: pqueue}} end def optimal_dist(state, node) do state.dist |> Enum.filter(fn {{pos, _}, _} -> pos == node end) |> Enum.map(fn {_, v} -> v end) |> Enum.min() end def optimal_path_nodes(state, node) do nodes = state.dist |> Enum.filter(fn {{pos, _}, _} -> pos == node end) mindist = nodes |> Enum.map(fn {_, v} -> v end) |> Enum.min() nodes = nodes |> Enum.filter(fn {_, v} -> v == mindist end) |> Enum.map(fn {posdir, _} -> posdir end) optimal_path_nodes(state, nodes, []) end defp optimal_path_nodes(state, queue, acc) do case queue do [] -> acc |> Enum.map(fn {pos, _} -> pos end) |> Enum.uniq() [h | t] -> acc = [h | acc] queue = Map.fetch!(state.path, h) ++ t optimal_path_nodes(state, queue, acc) end end end defmodule Game do @derive Inspect defstruct [:grid, :start, :end] def from_data(data) do grid = data |> String.split("\n", trim: true) |> Enum.with_index() |> Enum.flat_map(fn {s, i} -> String.codepoints(s) |> Enum.with_index() |> Enum.map(fn {c, j} -> {{i, j}, c} end) end) |> Map.new() start = grid |> Enum.find_value(fn {pos, c} -> c == "S" and pos end) |> then(& {&1, :right}) endpos = grid |> Enum.find_value(fn {pos, c} -> c == "E" and pos end) %Game{grid: grid, start: start, end: endpos} end def shortest(game) do game |> shortest(State.new(game)) |> State.optimal_dist(game.end) end def shortest_path_nodes(game) do game |> shortest(State.new(game)) |> State.optimal_path_nodes(game.end) end defp shortest(game, state) do if State.empty?(state) do state else {{dist, {pos, dir}}, state} = State.pop(state) if pos == game.end do shortest(game, state) else neighbors(game, {dist, {pos, dir}}) for {ndist, nposdir} <- neighbors(game, {dist, {pos, dir}}), reduce: state do state -> State.upsert(state, nposdir, ndist, {pos, dir}) end |> then(& shortest(game, &1)) end end end defp neighbors(game, {dist, {{i, j}, dir}}) do case dir do :right -> [{dist+1, {{i, j+1}, :right}}, {dist+1001, {{i-1, j}, :up}}, {dist+1001, {{i+1, j}, :down}}, {dist+2001, {{i, j-1}, :left}}] :left -> [{dist+1, {{i, j-1}, :left}}, {dist+1001, {{i-1, j}, :up}}, {dist+1001, {{i+1, j}, :down}}, {dist+2001, {{i, j+1}, :right}}] :up -> [{dist+1, {{i-1, j}, :up}}, {dist+1001, {{i, j-1}, :left}}, {dist+1001, {{i, j+1}, :right}}, {dist+2001, {{i+1, j}, :down}}] :down -> [{dist+1, {{i+1, j}, :down}}, {dist+1001, {{i, j-1}, :left}}, {dist+1001, {{i, j+1}, :right}}, {dist+2001, {{i-1, j}, :up}}] end |> Enum.filter(fn {_, {p, _}} -> case Map.fetch(game.grid, p) do :error -> false {:ok, "#"} -> false _ -> true end end) end end def part1(data) do data |> Game.from_data() |> Game.shortest() end def part2(data) do data |> Game.from_data() |> Game.shortest_path_nodes() |> Enum.count() end end data = IO.read(:stdio, :eof) {time1, ans1} = :timer.tc(fn -> Day16.part1(data) end) IO.puts("Time : #{time1 / 1000000}") IO.puts("Answer: #{ans1}") {time2, ans2} = :timer.tc(fn -> Day16.part2(data) end) IO.puts("Time : #{time2 / 1000000}") IO.puts("Answer: #{ans2}")