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

Advent of Code 2023 Day 17 Alternative

2023/day-17-alt.livemd

Advent of Code 2023 Day 17 Alternative

Mix.install([
  {:kino_aoc, "~> 0.1.5"}
])

Section

defmodule PairingHeap do
  @moduledoc """
  Pairing heap storing key-value pairs sorted by key.
  there's no guarantee of the order of the pairs with the same key.

  ### Performance

  - `new/0`: O(1)
  - `insert/3`: O(1)
  - `delete_min/1`: Amortized O(log n)
  - `top/1`: O(1)
  """

  @opaque t(key, value) :: empty_heap() | non_empty_heap(key, value)
  @opaque empty_heap() :: nil
  @opaque non_empty_heap(key, value) :: {key, value, [non_empty_heap(key, value)]}

  @doc """
  Creates an empty pairing heap.
  """
  @spec new() :: empty_heap()
  def new(), do: nil

  @doc """
  Insert a key-value pair into the heap.
  """
  @spec insert(t(key, value), key, value) :: t(key, value)
        when key: any(), value: any()
  def insert(heap, key, value) do
    meld(heap, {key, value, []})
  end

  @doc """
  Deletes the key-value pair with the smallest key from the heap.
  If there are multiple pairs with the same smallest key,
  it's not deterministic which pair will be deleted.

  ### Return value

  If the heap is empty, returns `{:error, :empty, original_heap}`,
  otherwise returns `{:ok, {deleted_key, deleted_value}, updated_heap}`.
  """
  @spec delete_min(t(key, value)) ::
          {:ok, {key, value}, t(key, value)} | {:error, :empty, empty_heap()}
        when key: any(), value: any()
  def delete_min(nil) do
    {:error, :empty, nil}
  end

  def delete_min({k, v, children}) do
    {:ok, {k, v}, pair(children)}
  end

  @doc """
  Gets the key-value pair with the smallest key from the heap.
  If there are multiple pairs with the same smallest key,
  it's not deterministic which pair will be returned.

  ### Return value

  If the heap is empty, returns `{:error, :empty}`,
  otherwise returns `{:ok, {key, value}}`.
  """
  @spec top(t(key, value)) :: {:ok, {key, value}} | {:error, :empty}
        when key: any(), value: any()
  def top(nil) do
    {:error, :empty}
  end

  def top({k, v, _}) do
    {:ok, {k, v}}
  end

  @spec pair([t(key, value)]) :: t(key, value)
        when key: any(), value: any()
  defp pair([]), do: nil

  defp pair([h]), do: h

  defp pair([h1, h2 | t]) do
    meld(meld(h1, h2), pair(t))
  end

  @spec meld(t(key, value), t(key, value)) :: t(key, value)
        when key: any(),
             value: any()
  defp meld(nil, heap), do: heap

  defp meld(heap, nil), do: heap

  defp meld({k1, v1, c1} = h1, {k2, v2, c2} = h2) do
    if k1 < k2 do
      {k1, v1, [h2 | c1]}
    else
      {k2, v2, [h1 | c2]}
    end
  end
end
defmodule PriorityQueue do
  @moduledoc """
  A priority queue implementation based on pairing heap
  that provides

  - an amortized O(log n) soft deletion mechanism.
  - an amortized O(log n) decrease-key operation (which is called `decrease_nicety` in the API).
  """

  alias PairingHeap, as: H

  @opaque t(item) :: {
            insertions :: H.t(integer(), item),
            deletions :: H.t(integer(), item)
          }

  @spec new() :: t(any())
  def new() do
    {H.new(), H.new()}
  end

  @spec insert(t(item), item, integer()) :: t(item) when item: any()
  def insert({insertions, deletions}, item, nicety) do
    {H.insert(insertions, nicety, item), deletions}
  end

  @spec delete(t(item), item, integer()) :: t(item) when item: any()
  def delete({insertions, deletions}, item, nicety) do
    {insertions, H.insert(deletions, nicety, item)}
  end

  @spec pop(t(item)) :: {:error, :empty} | {:ok, {item, integer()}, t(item)} when item: any()
  def pop({insertions, deletions}) do
    itop = H.top(insertions)
    dtop = H.top(deletions)

    cond do
      itop == {:error, :empty} ->
        {:error, :empty}

      itop != dtop ->
        {:ok, {nicety, item}, insertions} = H.delete_min(insertions)
        {:ok, {item, nicety}, {insertions, deletions}}

      true ->
        {_, _, insertions} = H.delete_min(insertions)
        {_, _, deletions} = H.delete_min(deletions)
        pop({insertions, deletions})
    end
  end

  @spec decrease_nicety(t(item), item, integer(), integer()) :: t(item) when item: any()
  def decrease_nicety(pq, _item, old_nicety, new_nicety) when old_nicety <= new_nicety do
    pq
  end

  def decrease_nicety(pq, item, old_nicety, new_nicety) do
    pq
    |> delete(item, old_nicety)
    |> insert(item, new_nicety)
  end
end
{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2023", "17", System.fetch_env!("LB_AOC_SESSION"))
grid =
  for {row, i} <- puzzle_input |> String.split() |> Stream.with_index(),
      {val, j} <- row |> String.to_charlist() |> Stream.with_index(),
      into: %{},
      do: {{i, j}, val - ?0}
defmodule AoC2023.Day17 do
  alias PriorityQueue, as: PQ

  @spec part1(%{coord => loss}) :: total_loss
        when coord: {i :: non_neg_integer(), j :: non_neg_integer()},
             loss: pos_integer(),
             total_loss: pos_integer()
  def part1(grid) do
    {dest, _} = Enum.max(grid)

    PQ.new()
    |> PQ.insert({{0, 1}, {0, 1, 1}}, grid[{0, 1}])
    |> PQ.insert({{1, 0}, {1, 0, 1}}, grid[{1, 0}])
    |> total_loss_p1(
      grid,
      dest,
      %{
        {{0, 1}, {0, 1, 1}} => grid[{1, 0}],
        {{1, 0}, {1, 0, 1}} => grid[{0, 1}]
      }
    )
  end

  @spec part2(%{coord => loss}) :: total_loss
        when coord: {i :: non_neg_integer(), j :: non_neg_integer()},
             loss: pos_integer(),
             total_loss: pos_integer()
  def part2(grid) do
    {dest, _} = Enum.max(grid)

    PQ.new()
    |> PQ.insert({{0, 1}, {0, 1, 1}}, grid[{0, 1}])
    |> PQ.insert({{1, 0}, {1, 0, 1}}, grid[{1, 0}])
    |> total_loss_p2(
      grid,
      dest,
      %{
        {{0, 1}, {0, 1, 1}} => grid[{1, 0}],
        {{1, 0}, {1, 0, 1}} => grid[{0, 1}]
      }
    )
  end

  defp total_loss_p1(pq, grid, dest, seen) do
    case PQ.pop(pq) do
      {:ok, {{^dest, _}, loss}, _pq} ->
        loss

      {:ok, {{{i, j}, {di, dj, steps}}, loss}, pq} ->
        {i2, j2} = {i + dj, j - di}
        pds = {{i2, j2}, {dj, -di, 1}}
        new_nicety = grid[{i2, j2}] &amp;&amp; loss + grid[{i2, j2}]

        {pq, seen} =
          if new_nicety do
            case seen[pds] do
              nil ->
                pq = PQ.insert(pq, pds, new_nicety)
                seen = Map.put(seen, pds, new_nicety)
                {pq, seen}

              old_nicety when old_nicety > new_nicety ->
                pq = PQ.decrease_nicety(pq, pds, old_nicety, new_nicety)
                seen = %{seen | pds => new_nicety}
                {pq, seen}

              _ ->
                {pq, seen}
            end
          else
            {pq, seen}
          end

        {i2, j2} = {i - dj, j + di}
        pds = {{i2, j2}, {-dj, di, 1}}
        new_nicety = grid[{i2, j2}] &amp;&amp; loss + grid[{i2, j2}]

        {pq, seen} =
          if new_nicety do
            case seen[pds] do
              nil ->
                pq = PQ.insert(pq, pds, new_nicety)
                seen = Map.put(seen, pds, new_nicety)
                {pq, seen}

              old_nicety when old_nicety > new_nicety ->
                pq = PQ.decrease_nicety(pq, pds, old_nicety, new_nicety)
                seen = %{seen | pds => new_nicety}
                {pq, seen}

              _ ->
                {pq, seen}
            end
          else
            {pq, seen}
          end

        {i2, j2} = {i + di, j + dj}
        pds = {{i2, j2}, {di, dj, steps + 1}}
        new_nicety = grid[{i2, j2}] &amp;&amp; loss + grid[{i2, j2}]

        {pq, seen} =
          if steps < 3 &amp;&amp; new_nicety do
            case seen[pds] do
              nil ->
                pq = PQ.insert(pq, pds, new_nicety)
                seen = Map.put(seen, pds, new_nicety)
                {pq, seen}

              old_nicety when old_nicety > new_nicety ->
                pq = PQ.decrease_nicety(pq, pds, old_nicety, new_nicety)
                seen = %{seen | pds => new_nicety}
                {pq, seen}

              _ ->
                {pq, seen}
            end
          else
            {pq, seen}
          end

        total_loss_p1(pq, grid, dest, seen)
    end
  end

  defp total_loss_p2(pq, grid, dest, seen) do
    case PQ.pop(pq) do
      {:ok, {{^dest, _}, loss}, _pq} ->
        loss

      {:ok, {{{i, j}, {di, dj, steps}}, loss}, pq} ->
        {i2, j2} = {i + dj, j - di}
        pds = {{i2, j2}, {dj, -di, 1}}
        new_nicety = grid[{i2, j2}] &amp;&amp; loss + grid[{i2, j2}]

        {pq, seen} =
          if steps >= 4 &amp;&amp; new_nicety do
            case seen[pds] do
              nil ->
                pq = PQ.insert(pq, pds, new_nicety)
                seen = Map.put(seen, pds, new_nicety)
                {pq, seen}

              old_nicety when old_nicety > new_nicety ->
                pq = PQ.decrease_nicety(pq, pds, old_nicety, new_nicety)
                seen = %{seen | pds => new_nicety}
                {pq, seen}

              _ ->
                {pq, seen}
            end
          else
            {pq, seen}
          end

        {i2, j2} = {i - dj, j + di}
        pds = {{i2, j2}, {-dj, di, 1}}
        new_nicety = grid[{i2, j2}] &amp;&amp; loss + grid[{i2, j2}]

        {pq, seen} =
          if steps >= 4 &amp;&amp; new_nicety do
            case seen[pds] do
              nil ->
                pq = PQ.insert(pq, pds, new_nicety)
                seen = Map.put(seen, pds, new_nicety)
                {pq, seen}

              old_nicety when old_nicety > new_nicety ->
                pq = PQ.decrease_nicety(pq, pds, old_nicety, new_nicety)
                seen = %{seen | pds => new_nicety}
                {pq, seen}

              _ ->
                {pq, seen}
            end
          else
            {pq, seen}
          end

        {i2, j2} = {i + di, j + dj}
        pds = {{i2, j2}, {di, dj, steps + 1}}
        new_nicety = grid[{i2, j2}] &amp;&amp; loss + grid[{i2, j2}]

        {pq, seen} =
          if steps < 10 &amp;&amp; new_nicety do
            case seen[pds] do
              nil ->
                pq = PQ.insert(pq, pds, new_nicety)
                seen = Map.put(seen, pds, new_nicety)
                {pq, seen}

              old_nicety when old_nicety > new_nicety ->
                pq = PQ.decrease_nicety(pq, pds, old_nicety, new_nicety)
                seen = %{seen | pds => new_nicety}
                {pq, seen}

              _ ->
                {pq, seen}
            end
          else
            {pq, seen}
          end

        total_loss_p2(pq, grid, dest, seen)
    end
  end
end
AoC2023.Day17.part1(grid)
AoC2023.Day17.part2(grid)