Powered by AppSignal & Oban Pro

AoC, Day12

2022/elixir/day12.livemd

AoC, Day12

Mix.install([
  {:kino_aoc, git: "https://github.com/ljgago/kino_aoc"}
])

Section

{:ok, data} = KinoAOC.download_puzzle("2022", "12", System.fetch_env!("LB_AOC_SECRET"))

Solve

defmodule Day12 do
  @nbh [{-1, 0}, {1, 0}, {0, -1}, {0, 1}]
  @st 0
  @en ?z - ?a + 1 + 1

  def solve(data, opts \\ []) do
    debug? = Access.get(opts, :debug, false)

    dd = data |> String.split("\n", trim: true) |> Enum.map(&String.graphemes/1)
    en = get_end(dd)
    st1 = get_start(dd) |> i("inputs task 1", debug?)
    st2 = get_start(dd, task: 2) |> i("inputs task 2", debug?)
    vis = MapSet.new()

    map = prep_space(dd)

    with {:ok, d1, ^en} <- run(map, st1, vis),
         {:ok, d2, ^en} <- run(map, st2, vis) do
      {d1, d2}
    else
      error -> error
    end
  end

  def run(_m, {[], []}, vis), do: {:not_found, vis}

  def run(map, q, visited) do
    {{:value, {cur, dist} = _el}, q} = :queue.out(q)

    h = Map.get(map, cur)
    seen = MapSet.member?(visited, cur)

    do_run(map, q, visited, h, seen, cur, dist)
  end

  def do_run(_map, _q, _vis, h, _seen, cur, dist) when h == @en, do: {:ok, dist, cur}
  def do_run(map, q, vis, _h, true, _cur, _dist), do: run(map, q, vis)

  def do_run(map, q, vis, h, false, {x, y} = cur, dist) do
    visited = MapSet.put(vis, cur)

    q =
      @nbh
      |> Enum.map(fn {i, j} ->
        nxt_h = Map.get(map, {x + i, y + j})
        {nxt_h, h, {x + i, y + j}}
      end)
      |> Enum.filter(&valid_next_step/1)
      |> Enum.reduce(q, fn {_, _, nxt}, acc ->
        :queue.in({nxt, dist + 1}, acc)
      end)

    run(map, q, visited)
  end

  def valid_next_step({nil, _b, _nxt}), do: false
  def valid_next_step({a, b, _nxt}) when a - b <= 1, do: true
  def valid_next_step({_a, _b, _nxt}), do: false

  def prep_space(dd) do
    dd
    |> Enum.with_index()
    |> Enum.flat_map(fn {row, i} ->
      row
      |> Enum.with_index()
      |> Enum.map(fn {c, j} -> {{i, j}, to_h(c)} end)
    end)
    |> Map.new()
  end

  def to_h("S"), do: @st
  def to_h("E"), do: @en
  def to_h(<<c>>), do: c - ?a + 1

  def get_end(dd) do
    dd
    |> Enum.with_index()
    |> Enum.map(fn {row, i} -> {i, Enum.find_index(row, &(&1 == "E"))} end)
    |> Enum.reject(fn {_x, y} -> is_nil(y) end)
    |> List.first()
  end

  def get_start(dd, opts \\ []) do
    task = Access.get(opts, :task, 1)

    dd
    |> Enum.with_index()
    |> Enum.flat_map(&start_finder(&1, task))
    |> Enum.reject(fn {_x, y} -> is_nil(y) end)
    |> Enum.map(fn el -> {el, 0} end)
    |> Enum.reduce(:queue.new(), &:queue.in/2)
  end

  def start_finder({row, i}, 1), do: [{i, Enum.find_index(row, &(&1 == "S"))}]

  def start_finder({row, i}, 2) do
    row
    |> Enum.with_index()
    |> Enum.map(fn {c, j} -> {i, (c == "a" && j) || nil} end)
  end

  def i(arg, _, false), do: arg
  def i(arg, msg, true), do: IO.inspect(arg, label: msg)
end

# {534, 525}
Day12.solve(data)

Test

ExUnit.start(autorun: false)

defmodule AoCTest do
  use ExUnit.Case, async: false

  @tdata """
  Sabqponm
  abcryxxl
  accszExk
  acctuvwj
  abdefghi
  """

  test "solves tasks" do
    assert {31, 29} = Day12.solve(@tdata)
  end
end

ExUnit.run()