Powered by AppSignal & Oban Pro

Advent of Code 2022

2022/day12.livemd

Advent of Code 2022

Mix.install([
  {:req, "~> 0.3.2"}
])

Day 12

input =
  "https://adventofcode.com/2022/day/12/input"
  |> Req.get!(headers: [cookie: "session=#{System.get_env("AOC_COOKIE")}"])
  |> Map.get(:body)
sample = """
Sabqponm
abcryxxl
accszExk
acctuvwj
abdefghi
"""
# input = sample
state =
  input
  |> String.split("\n", trim: true)
  |> Enum.with_index()
  |> Enum.reduce(%{map: Map.new(), cost: Map.new()}, fn {line, y}, acc ->
    line
    |> String.split("", trim: true)
    |> Enum.with_index()
    |> Enum.reduce(acc, fn {letter, x}, acc ->
      c = {x, y}

      case letter do
        "S" ->
          acc
          |> Map.put(:open, Map.put(%{}, c, 0))
          |> put_in([:map, c], ?a)

        "E" ->
          acc
          |> Map.put(:end, c)
          |> put_in([:map, c], ?z)

        _ ->
          put_in(acc, [:map, c], String.to_charlist(letter) |> hd())
      end
    end)
  end)

Part 1

Stream.iterate(0, &(&1 + 1))
|> Enum.reduce_while(state, fn _, acc ->
  case acc.open do
    open when open == %{} ->
      {:halt, acc}

    open ->
      {{x, y} = c, cost} = Enum.min_by(open, fn {_, cost} -> cost end)

      if c == state.end do
        acc
        |> put_in([:cost, c], cost)
        |> then(&{:halt, &1})
      else
        # visit
        acc =
          acc
          |> put_in([:cost, c], cost)
          |> Map.put(:open, Map.delete(open, c))

        h = Map.get(acc.map, c)

        # expand
        [
          {1, 0},
          {-1, 0},
          {0, 1},
          {0, -1}
        ]
        |> Enum.map(fn {dx, dy} -> {x + dx, y + dy} end)
        |> Enum.filter(fn c2 ->
          case Map.get(acc.map, c2) do
            nil -> false
            h2 -> h2 - h <= 1
          end
        end)
        # if cost is less, let's explore
        |> Enum.filter(fn c ->
          case Map.get(acc.cost, c) do
            nil -> true
            cost0 -> cost + 1 < cost0
          end
        end)
        |> Enum.reduce(acc, fn c2, acc ->
          Map.update!(acc, :open, &amp;Map.put(&amp;1, c2, cost + 1))
        end)
        |> then(&amp;{:cont, &amp;1})
      end
  end
end)
|> then(fn state ->
  Map.get(state.cost, state.end)
end)

Part 2

state.map
|> Enum.filter(fn {_, v} -> v == ?a end)
|> Enum.map(fn {c, _} -> c end)
|> Enum.map(fn c ->
  open = Map.new() |> Map.put(c, 0)
  state = Map.put(state, :open, open)

  Stream.iterate(0, &amp;(&amp;1 + 1))
  |> Enum.reduce_while(state, fn _, acc ->
    case acc.open do
      open when open == %{} ->
        {:halt, acc}

      open ->
        {{x, y} = c, cost} = Enum.min_by(open, fn {_, cost} -> cost end)

        if c == state.end do
          acc
          |> put_in([:cost, c], cost)
          |> then(&amp;{:halt, &amp;1})
        else
          # visit
          acc =
            acc
            |> put_in([:cost, c], cost)
            |> Map.put(:open, Map.delete(open, c))

          h = Map.get(acc.map, c)

          # expand
          [
            {1, 0},
            {-1, 0},
            {0, 1},
            {0, -1}
          ]
          |> Enum.map(fn {dx, dy} -> {x + dx, y + dy} end)
          |> Enum.filter(fn c2 ->
            case Map.get(acc.map, c2) do
              nil -> false
              h2 -> h2 - h <= 1
            end
          end)
          # if cost is less, let's explore
          |> Enum.filter(fn c ->
            case Map.get(acc.cost, c) do
              nil -> true
              cost0 -> cost + 1 < cost0
            end
          end)
          |> Enum.reduce(acc, fn c2, acc ->
            Map.update!(acc, :open, &amp;Map.put(&amp;1, c2, cost + 1))
          end)
          |> then(&amp;{:cont, &amp;1})
        end
    end
  end)
  |> then(fn state ->
    Map.get(state.cost, state.end)
  end)
end)
|> Enum.min()

Part 2 is pretty slow, took 81.6s. Maybe need some kind of priority queue.