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

Day 16

day16.livemd

Day 16

Mix.install([
  {:kino, "~> 0.14.2"},
  {:kino_aoc, "~> 0.1.7"},
  {:libgraph, "~> 0.16.0"}
])

Part 1

example = """
#################
#...#...#...#..E#
#.#.#.#.#.#.#.#.#
#.#.#.#...#...#.#
#.#.#.#.###.#.#.#
#...#.#.#.....#.#
#.#.#.#.#.#####.#
#.#...#.#.#.....#
#.#.#####.#.###.#
#.#.#.......#...#
#.#.###.#####.###
#.#.#...#.....#.#
#.#.#.#####.###.#
#.#.#.........#.#
#.#.#.#########.#
#S#.............#
#################
"""

{:ok, input} = if true do
  KinoAOC.download_puzzle("2024", "16", System.fetch_env!("LB_AOC_SESSION"))
else
  {:ok, example}
end
  
grid = input
  |> String.trim()
  |> String.split("\n")
  |> Enum.map(&String.graphemes/1)

# Grid with values
val_grid = for {row, i} <- Enum.with_index(grid) do
  for {val, j} <- Enum.with_index(row) do
    {{i, j}, val}
  end
end |> List.flatten()

# Cost grid with enumerated directions
cost_grid = for {{i, j}, _v} <- val_grid do
  for dir <- [:v, :>, :<, :^] do
    {{i, j, dir}, :inf}
  end
end |> List.flatten() |> Enum.into(%{})

{{start_i, start_j}, _val} = List.keyfind(val_grid, "S", 1)
{{end_i, end_j}, _val} = List.keyfind(val_grid, "E", 1)

val_grid = Enum.into(val_grid, %{})
defmodule Part1 do
  # Get new direction after a turn
  def turn(:>, turn), do: if(turn == :l, do: :^, else: :v)
  def turn(:v, turn), do: if(turn == :l, do: :>, else: :<)
  def turn(:<, turn), do: if(turn == :l, do: :v, else: :^)
  def turn(:^, turn), do: if(turn == :l, do: :<, else: :>)

  # Get next location in a given direction
  def get_next_loc(dir, {i, j}) do
    case dir do
      :> -> {i, j + 1}
      :< -> {i, j - 1}
      :^ -> {i - 1, j}
      :v -> {i + 1, j}
    end
  end

  # Get non-wall neighbors, costs, and directions
  def get_neighbors(dir, loc, val_grid) do
    ldir = turn(dir, :l)
    rdir = turn(dir, :r)

    f = {dir |> get_next_loc(loc), 1, dir}
    l = {ldir |> get_next_loc(loc), 1001, ldir}
    r = {rdir |> get_next_loc(loc), 1001, rdir}

    # Filter walls; return list of valid neighbors
    Enum.filter([f, l, r], fn {loc1, _c1, _d1} ->
      val = Map.fetch!(val_grid, loc1)
      val != "#"
    end)
  end
  
  def walk_grid([], cost_grid, _), do: cost_grid
  
  def walk_grid([{{i, j}, _c, dir} | rest_to_check], cost_grid, val_grid) do
    my_cost = Map.get(cost_grid, {i, j, dir})
    
    # Find neighbors who have a lower cost path than previously known
    neighs_to_check = get_neighbors(dir, {i, j}, val_grid)
      |> Enum.filter(fn {{i, j}, ncost, ndir} ->
        ncost + my_cost < Map.fetch!(cost_grid, {i, j, ndir})
      end)

    # Update cost grid with new values
    cost_grid = Enum.reduce(neighs_to_check, cost_grid, fn {{i, j}, ncost, ndir}, acc ->
      Map.replace!(acc, {i, j, ndir}, ncost + my_cost)
    end)

    # Continue walking neighbors
    walk_grid(neighs_to_check ++ rest_to_check, cost_grid, val_grid)
  end
end

cost_grid = Map.replace!(cost_grid, {start_i, start_j, :>}, 0)
cost_grid = Part1.walk_grid([{{start_i, start_j}, 0, :>}], cost_grid, val_grid)
result = for dir <- [:v, :>, :<, :^] do
  Map.get(cost_grid, {end_i, end_j, dir})
end |> Enum.min()