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

Advent of code day 16

2024/livebooks/day-16.livemd

Advent of code day 16

Mix.install([
  {:kino, "~> 0.5.0"}
])

Setup input

example = Kino.Input.textarea("Please paste your input example:")
input = Kino.Input.textarea("Please paste your real input:")
parsed =
  example
  |> Kino.Input.read()
  |> String.split("\n", trim: true)
  |> Enum.map(&(String.split(&1, "", trim: true) |> List.to_tuple()))
  |> List.to_tuple()

lines = tuple_size(parsed) - 1
cols = tuple_size(elem(parsed, 0)) - 1

grid =
  for l <- 0..lines, c <- 0..cols, into: %{} do
    {{l, c}, elem(elem(parsed, l), c)}
  end


{start_position, _ } = Enum.find(grid, fn {_, v} -> v == "S" end)
{end_position, _ } = Enum.find(grid, fn {_, v} -> v == "E" end)
defmodule PriorityQueue do
  @moduledoc """
  A stateless priority queue implementation basing on pairing heap.
  """

  @initial_serial -Bitwise.bsl(1, 64)

  defstruct size: 0,
            serial: @initial_serial,
            heap: nil,
            key_fun: nil

  @typep serial :: integer()
  @typep pairing_heap(key, value) :: nil | {{key, serial()}, value, [pairing_heap(key, value)]}
  @opaque t(key, value) :: %__MODULE__{
            size: non_neg_integer,
            serial: serial(),
            heap: pairing_heap(key, value),
            key_fun: (value -> key)
          }

  @doc """
  Create an empty priority queue.
  """
  @spec new((value -> key)) :: t(key, value) when key: any(), value: any()
  def new(key_fun \\ nil), do: %__MODULE__{key_fun: key_fun || (&amp; &amp;1)}

  @doc """
  Get the size of a priority queue.
  """
  @spec size(t(any, any)) :: non_neg_integer()
  def size(%__MODULE__{size: size}), do: size

  @doc """
  Put an item into a priority queue.
  Returns a new priority queue.
  """
  @spec push(t(key, value), value) :: t(key, value) when key: any(), value: any()
  def push(%__MODULE__{size: size, heap: heap, key_fun: key_fun, serial: serial} = pq, value) do
    %{pq | size: size + 1, heap: meld(heap, to_heap(value, key_fun, serial)), serial: serial + 1}
  end

  @doc """
  Get and delete the smallest item in the priority queue.
  If the queue is empty, returns `{:error, :empty}`,
  otherwise returns `{:ok, smallest_item, new_priority_queue}`.
  """
  @spec pop(t(key, value)) :: {:ok, value, t(key, value)} | {:error, :empty}
        when key: any(), value: any()
  def pop(%__MODULE__{size: 0}), do: {:error, :empty}

  def pop(%__MODULE__{size: size, heap: {_key, value, sub_heaps}} = pq) do
    {:ok, value, %{pq | size: size - 1, heap: pair(sub_heaps)}}
  end

  @doc """
  Get the smallest element in the priority queue.
  """
  @spec top(t(key, value)) :: {:ok, value} | {:error, :empty}
        when key: any(), value: any()
  def top(%__MODULE__{size: 0}), do: {:error, :empty}
  def top(%__MODULE__{heap: {_, value, _}}), do: {:ok, value}

  @doc """
  Merge two priority queues. Crashes if the given priority queues have different key functions.
  """
  @spec merge!(t(key, value), t(key, value)) :: t(key, value)
        when key: any(), value: any()
  def merge!(%__MODULE__{key_fun: f} = pq1, %__MODULE__{key_fun: f} = pq2) do
    %__MODULE__{
      key_fun: f,
      serial: max(pq1.serial, pq2.serial),
      size: pq1.size + pq2.size,
      heap: meld(pq1.heap, pq2.heap)
    }
  end

  def merge!(%__MODULE__{}, %__MODULE__{}) do
    raise ArgumentError, "Can't merge priority queues with different key functions."
  end

  defp to_heap(value, key_fun, serial) do
    {{key_fun.(value), serial}, value, []}
  end

  @spec meld(pairing_heap(key, value), pairing_heap(key, value)) :: pairing_heap(key, value)
        when key: any(), value: any()
  defp meld(nil, heap), do: heap
  defp meld(heap, nil), do: heap
  defp meld({k1, v1, sub1}, {k2, _v2, _sub2} = heap2) when k1 < k2, do: {k1, v1, [heap2 | sub1]}
  defp meld(heap1, {k2, v2, sub2}), do: {k2, v2, [heap1 | sub2]}

  @spec pair([pairing_heap(key, value)]) :: pairing_heap(key, value)
        when key: any(), value: any()
  defp pair([]), do: nil
  defp pair([node]), do: node
  defp pair([node1, node2 | rest]), do: meld(meld(node2, node1), pair(rest))
end
defmodule Dijkstra do
  def solve(graph, origin, destination) when is_map(graph) do
    pq = PriorityQueue.new() |> PriorityQueue.push({0, {0, 1}, [origin]})
    shortest_path(graph, pq, destination, MapSet.new())
  end

  def shortest_path(_graph, [] = _path, _destination, _visited) do
    {0, []}
  end

  def shortest_path(_graph, [{cost, _dir, [destination | _] = path} | _], destination, _visited) do
    {cost, :lists.reverse(path)}
  end


  def shortest_path(graph, pq, destination, visited) do
    {:ok, {cost, {dr, dc}, [{r, c} | _] = path}, pq} = PriorityQueue.pop(pq)

    if {r, c} == destination do
      {:ok, cost, path}
    else
      visited = MapSet.put(visited, {r, c, dr, dc})

      pq =
        for {new_cost, {nr, nc}, {ndr, ndc}} <- [
              {1, {r + dr, c + dc}, {dr, dc}},
              {1000, {r, c}, {dc, -dr}},
              {1000, {r, c}, {-dc, dr}}
            ],
            not MapSet.member?(visited, {nr, nc, ndr, ndc}),
            graph[{r, c}] !== "#" do
          {cost + new_cost, {ndr, ndc}, [{nr, nc} | path]}
        end
        |> Enum.reject(&amp;(&amp;1 == nil))
        |> Enum.reduce(pq, fn new_route, p ->
          PriorityQueue.push(p, new_route)
        end)

      shortest_path(
        graph,
        pq,
        destination,
        visited
      )
    end
  end
end

Part 01

Dijkstra.solve(grid, start_position, end_position)

Part 02