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

Day 16

2024/day16.livemd

Day 16

Mix.install([{:utils, path: "#{__DIR__}/utils"}, {:heap, "~> 3.0"}])

Solution

defmodule Cell do
  defstruct [:pos, :dir]

  def new(pos, dir), do: %Cell{pos: pos, dir: dir}

  def nbrs(%Cell{pos: {x, y}}) do
    [
      Cell.new({x - 1, y}, :north),
      Cell.new({x + 1, y}, :south),
      Cell.new({x, y + 1}, :east),
      Cell.new({x, y - 1}, :west)
    ]
  end
end
defmodule Part1 do
  def dijkstra(grid, heap, seen) do
    {score, curr} = Heap.root(heap)
    heap = Heap.pop(heap)
    val = Map.get(grid, curr.pos)

    cond do
      val == ?E ->
        score

      val == ?# or curr in seen ->
        dijkstra(grid, heap, seen)

      true ->
        add_nbr = fn nbr, acc ->
          score_ = if nbr.dir == curr.dir, do: score + 1, else: score + 1001
          Heap.push(acc, {score_, nbr})
        end

        heap_ = curr |> Cell.nbrs() |> Enum.reduce(heap, add_nbr)
        dijkstra(grid, heap_, MapSet.put(seen, curr))
    end
  end
end

grid = "day16" |> Utils.read_grid() |> elem(0) |> Map.new()
start = grid |> Enum.find(fn {_, v} -> v == ?S end) |> elem(0)
heap = Heap.min() |> Heap.push({0, Cell.new(start, :east)})

best_score = Part1.dijkstra(grid, heap, MapSet.new())
defmodule Part2 do
  def dijkstra(target, grid, heap, cache) do
    if Heap.empty?(heap) do
      cache
    else
      {score, curr, prev} = Heap.root(heap)
      heap = Heap.pop(heap)
      val = Map.get(grid, curr.pos)

      {cached_score, cached_tiles} = Map.get(cache, curr, {target, MapSet.new()})
      {_, prev_tiles} = Map.get(cache, prev, {nil, MapSet.new()})
      tiles = prev_tiles |> MapSet.put(curr) |> MapSet.union(cached_tiles)
      cache_ = Map.put(cache, curr, {score, tiles})

      cond do
        val == ?# or score > cached_score ->
          dijkstra(target, grid, heap, cache)

        score == cached_score ->
          dijkstra(target, grid, heap, cache_)

        true ->
          add_nbr = fn nbr, acc ->
            score_ = if nbr.dir == curr.dir, do: score + 1, else: score + 1001
            Heap.push(acc, {score_, nbr, curr})
          end

          heap_ = curr |> Cell.nbrs() |> Enum.reduce(heap, add_nbr)
          dijkstra(target, grid, heap_, cache_)
      end
    end
  end
end

heap2 = Heap.min() |> Heap.push({0, Cell.new(start, :east), nil})
end_pos = grid |> Enum.find(fn {_, v} -> v == ?E end) |> elem(0)
end_cells = [:north, :south, :east, :west] |> Enum.map(&Cell.new(end_pos, &1))

Part2.dijkstra(best_score, grid, heap2, %{})
|> Map.take(end_cells)
|> Enum.map(fn {_, {_, xs}} -> xs |> Enum.into(MapSet.new(), & &1.pos) end)
|> Enum.reduce(&MapSet.union/2)
|> Enum.count()