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

AoC 2022 - Day 24

2022/day24.livemd

AoC 2022 - Day 24

Mix.install([:kino])

Setup

input = Kino.Input.textarea("file input")

Common

defmodule Day24 do
  def remove_first_and_last(l) do
    tl(l) |> Enum.reverse() |> tl() |> Enum.reverse()
  end

  def parse_line({line, r}) do
    line
    |> String.to_charlist()
    |> remove_first_and_last()
    |> Enum.with_index()
    |> Enum.map(fn {value, c} -> {{r, c}, value} end)
  end

  def get_internal_maze_dimentions(data) do
    lines = String.split(data, "\n")
    {length(lines) - 2, String.length(hd(lines)) - 2}
  end

  def shortest_path(map, start_rc, end_rc, start_time) do
    # We need 1 minute to go from the initial starting point to the maze.
    shortest_path(map, start_rc, end_rc, start_time + 1, MapSet.new(), :queue.new())
  end

  def shortest_path(map, start_rc, end_rc, start_time, visited, {[], []}) do
    if is_free(map, start_rc, start_time) do
      q = :queue.in({start_rc, start_time}, {[], []})
      shortest_path(map, start_rc, end_rc, start_time + 1, visited, q)
    else
      shortest_path(map, start_rc, end_rc, start_time + 1, visited, {[], []})
    end
  end

  def shortest_path(map, start_rc, end_rc, start_time, visited, q) do
    {{:value, {rc, t}}, q} = :queue.out(q)

    if rc == end_rc do
      t + 1
    else
      if {rc, t} not in visited do
        visited = MapSet.put(visited, {rc, t})
        q = add_next_steps(map, rc, t, q)
        shortest_path(map, start_rc, end_rc, start_time, visited, q)
      else
        shortest_path(map, start_rc, end_rc, start_time, visited, q)
      end
    end
  end

  # Private methods
  defp add_next_steps(map, {row, col}, t, q) do
    [{0, 1}, {0, -1}, {1, 0}, {-1, 0}, {0, 0}]
    |> Enum.map(fn {dr, dc} -> {row + dr, col + dc} end)
    |> Enum.filter(fn {row, col} ->
      row >= 0 and col >= 0 and row < map.rows and col < map.cols
    end)
    |> Enum.filter(fn rc -> is_free(map, rc, t + 1) end)
    |> Enum.reduce(q, fn rc, q -> :queue.in({rc, t + 1}, q) end)
  end

  defp is_free(map, {row, col}, start_time) do
    right_blizzard_c = Integer.mod(col - start_time, map.cols)
    left_blizzard_c = Integer.mod(col + start_time, map.cols)
    down_blizzard_r = Integer.mod(row - start_time, map.rows)
    up_blizzard_r = Integer.mod(row + start_time, map.rows)

    not (MapSet.member?(map.right, {row, right_blizzard_c}) or
           MapSet.member?(map.left, {row, left_blizzard_c}) or
           MapSet.member?(map.up, {up_blizzard_r, col}) or
           MapSet.member?(map.down, {down_blizzard_r, col}))
  end
end

Part 1

data = input |> Kino.Input.read() |> String.trim()

data_by_type =
  data
  |> String.split("\n")
  |> Day24.remove_first_and_last()
  |> Enum.with_index()
  |> Enum.flat_map(&amp;Day24.parse_line/1)
  |> Enum.group_by(&amp;elem(&amp;1, 1), &amp;elem(&amp;1, 0))
  |> Map.new(fn {k, v} -> {k, MapSet.new(v)} end)

{r, c} = Day24.get_internal_maze_dimentions(data)

map_info = %{
  left: Map.get(data_by_type, ?<),
  right: Map.get(data_by_type, ?>),
  up: Map.get(data_by_type, ?^),
  down: Map.get(data_by_type, ?v),
  rows: r,
  cols: c
}

Day24.shortest_path(map_info, {0, 0}, {map_info.rows - 1, map_info.cols - 1}, 0)

Part 2

data = input |> Kino.Input.read() |> String.trim()

data_by_type =
  data
  |> String.split("\n")
  |> Day24.remove_first_and_last()
  |> Enum.with_index()
  |> Enum.flat_map(&amp;Day24.parse_line/1)
  |> Enum.group_by(&amp;elem(&amp;1, 1), &amp;elem(&amp;1, 0))
  |> Map.new(fn {k, v} -> {k, MapSet.new(v)} end)

{r, c} = Day24.get_internal_maze_dimentions(data)

map_info = %{
  left: Map.get(data_by_type, ?<),
  right: Map.get(data_by_type, ?>),
  up: Map.get(data_by_type, ?^),
  down: Map.get(data_by_type, ?v),
  rows: r,
  cols: c
}

start_rc = {0, 0}
end_rc = {map_info.rows - 1, map_info.cols - 1}
t1 = Day24.shortest_path(map_info, start_rc, end_rc, 0)
t2 = Day24.shortest_path(map_info, end_rc, start_rc, t1)
Day24.shortest_path(map_info, start_rc, end_rc, t2)