defmodule Day20 do defmodule Maze do @derive Inspect @enforce_keys [:positions, :walls, :source, :target] defstruct @enforce_keys def from_data(data) do positions = data |> String.split("\n", trim: true) |> Enum.with_index() |> Enum.map(fn {l, i} -> l |> String.codepoints() |> Enum.with_index() |> Enum.map(fn {v, j} -> {{i, j}, v} end) end) |> List.flatten() source = Enum.find(positions, fn {_, v} -> v == "S" end) |> elem(0) target = Enum.find(positions, fn {_, v} -> v == "E" end) |> elem(0) walls = positions |> Enum.filter(fn {_, v} -> v == "#" end) |> Enum.map(fn {p, _} -> p end) positions = positions |> Enum.filter(fn {_, v} -> v != "#" end) |> Enum.map(fn {p, _} -> p end) |> MapSet.new() %Maze{positions: positions, walls: walls, source: source, target: target} end def cheats(maze, seconds) do # Get shortest distances for all nodes dist = rev_shortest(maze) # Get cheats for n1 <- maze.positions, n2 <- neighbors(maze, n1, seconds) do {n1, n2, Map.get(dist, n1) - Map.get(dist, n2) - dist(n1, n2)} end end defp dist({x1, y1}, {x2, y2}), do: abs(x1-x2) + abs(y1-y2) defp rev_shortest(maze) do maze.target |> then(&rev_shortest(maze, :queue.from_list([{&1, 0}]), MapSet.new([&1]), Map.new())) end defp rev_shortest(maze, q, inq, dist) do case :queue.peek(q) do :empty -> dist {_, {v, d}} -> for n <- neighbors(maze, v), reduce: {:queue.drop(q), inq} do {q, inq} -> if MapSet.member?(inq, n) do {q, inq} else {:queue.in({n, d+1}, q), MapSet.put(inq, n)} end end |> then(fn {q, inq} -> rev_shortest(maze, q, inq, Map.put(dist, v, d)) end) end end defp neighbors(maze, {x, y}) do [{x+1, y}, {x-1, y}, {x, y+1}, {x, y-1}] |> Enum.filter(&MapSet.member?(maze.positions, &1)) end defp neighbors(maze, {x, y}, d) do for i <- 0..d, j <- 0..d-i do cond do i == 0 and j == 0 -> [] i == 0 -> [{x, y+j}, {x, y-j}] j == 0 -> [{x+i, y}, {x-i, y}] true -> [{x+i, y+j}, {x-i, y+j}, {x+i, y-j}, {x-i, y-j}] end end |> List.flatten() |> Enum.filter(&MapSet.member?(maze.positions, &1)) end end def part1(data) do data |> Maze.from_data() |> Maze.cheats(2) |> Enum.count(& elem(&1, 2) >= 100) end def part2(data) do data |> Maze.from_data() |> Maze.cheats(20) |> Enum.count(& elem(&1, 2) >= 100) end end data = IO.read(:stdio, :eof) {time1, ans1} = :timer.tc(fn -> Day20.part1(data) end) IO.puts("Time : #{time1 / 1000000}") IO.puts("Answer: #{ans1}") {time2, ans2} = :timer.tc(fn -> Day20.part2(data) end) IO.puts("Time : #{time2 / 1000000}") IO.puts("Answer: #{ans2}")