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

Day 12 - Advent of Code 2022

lib/advent_of_code/2022/day-12.livemd

Day 12 - Advent of Code 2022

Mix.install([:kino, :benchee, :nx])
:ok

Links

Prompt

— Day 12: Hill Climbing Algorithm —

You try contacting the Elves using your handheld device, but the river you’re following must be too low to get a decent signal.

You ask the device for a heightmap of the surrounding area (your puzzle input). The heightmap shows the local area from above broken into a grid; the elevation of each square of the grid is given by a single lowercase letter, where a is the lowest elevation, b is the next-lowest, and so on up to the highest elevation, z.

Also included on the heightmap are marks for your current position (S) and the location that should get the best signal (E). Your current position (S) has elevation a, and the location that should get the best signal (E) has elevation z.

You’d like to reach E, but to save energy, you should do it in as few steps as possible. During each step, you can move exactly one square up, down, left, or right. To avoid needing to get out your climbing gear, the elevation of the destination square can be at most one higher than the elevation of your current square; that is, if your current elevation is m, you could step to elevation n, but not to elevation o. (This also means that the elevation of the destination square can be much lower than the elevation of your current square.)

For example:

Sabqponm
abcryxxl
accszExk
acctuvwj
abdefghi

Here, you start in the top-left corner; your goal is near the middle. You could start by moving down or right, but eventually you’ll need to head toward the e at the bottom. From there, you can spiral around to the goal:

v..v<<<<
>v.vv<<^
.>vv>E^^
..v>>>^^
..>>>>>^

In the above diagram, the symbols indicate whether the path exits each square moving up (^), down (v), left (<), or right (>). The location that should get the best signal is still E, and . marks unvisited squares.

This path reaches the goal in 31 steps, the fewest possible.

What is the fewest steps required to move from your current position to the location that should get the best signal?

To begin, get your puzzle input.

— Part Two —

As you walk up the hill, you suspect that the Elves will want to turn this into a hiking trail. The beginning isn’t very scenic, though; perhaps you can find a better starting point.

To maximize exercise while hiking, the trail should start as low as possible: elevation a. The goal is still the square marked E. However, the trail should still be direct, taking the fewest steps to reach its goal. So, you’ll need to find the shortest path from any square at elevation a to the square marked E.

Again consider the example from above:

Sabqponm
abcryxxl
accszExk
acctuvwj
abdefghi

Now, there are six choices for starting position (five marked a, plus the square marked S that counts as being at elevation a). If you start at the bottom-left square, you can reach the goal most quickly:

...v<<<<
...vv<<^
...v>E^^
.>v>>>^^
>^>>>>>^

This path reaches the goal in only 29 steps, the fewest possible.

What is the fewest steps required to move starting from any square with elevation a to the location that should get the best signal?

Although it hasn’t changed, you can still get your puzzle input.

Input

input = Kino.Input.textarea("Please paste your input file:")
input = input |> Kino.Input.read()
"abaaaaacccccccccccccccccccccccccccccccccccccccaaaaaaaccccaaaaaaaaaaaaaaaaacccccaaaaaacccccccccccccccccccccccaaaaaaaaccccccccccccccccccccccccccccccccaaaaaa\nabaaaaaacccaaaacccccccccccccccccccccccaccccccccaaaaaaaaccaaaaaaaaaaaaaaaaccccccaaaaaacccccccccccccccccccccccccaaaaccccccccccccccccccccccccccccccccccaaaaaa\nabaaaaaacccaaaacccccccccccccccccaaaaaaaacccccccaaaaaaaaacaaaaaaaaaaaaacccccccccaaaaacccccccccccccccccccccccccaaaaacccccccccccccccccccaaaccccccccccccaaaaaa\nabaaacaccccaaaaccccccccccccccccccaaaaaacccccccccaaaaaaaccccaaaaaaaaaaacccccccccaaaaacccccccccccccccccccccccccaacaaaccccccccccccccccccaaacccccccccccccccaaa\nabaaacccccccaaacccccccccccaacccccaaaaaaccccccccaaaaaaccccccaacaaaaaaaacccccccccccccccccccccccaaccccccccccccccacccaaaaacccccccccaaccccaaacccccccccccccccaaa\nabccccccccccccccccccccccccaaaaccaaaaaaaacccccccaaaaaaaccccccccaaaaaaaaaccccccccccaacccccccccaaaccccccccccccccccccacaaacccccccccaaaaccaaacccccccccccccccaac\nabccccccccccccccccccccccaaaaaacaaaaaaaaaaccccccaaccaaaaacccccaaaaccaaaaccccccccccaaacaacccccaaacaaacccaaccccccccaaaaaaaacccccccaaaaakkkkkkcccccccccccccccc\nabccccccccccccccccccccccaaaaaccaaaaaaaaaacccccccccccaaaaaaccccacccaaaaaccccccccccaaaaaaccaaaaaaaaaaaaaaaccccccccaaaaaaaaccccccccaaajkkkkkkkaccccccaacccccc\nabcccccccccccccccccccccccaaaaacacacaaaccccccccccccccaaaaaaccccccccaaaacccccccccaaaaaaacccaaaaaaaaaaaaaaaaaccccccccaaaaaccccccccccjjjkkkkkkkkccaaaaaacccccc\nabcccccccccccccccccccccccaacaacccccaaacccaccccccccccaaaaaaccccccccaaaacccccccccaaaaaaacccccaaaaaacaaaaaaaacccccccaaaaacccccccjjjjjjjooopppkkkcaaaaaaaccccc\nabcccccccccccccccccaacaacccccccccccaaaaaaacccccccccccaaaaacccccccccccccccccccccccaaaaaaccccaaaaaaccaaaaaaacccccccaaaaaacciijjjjjjjjoooopppkkkcaaaaaaaacccc\nabccccccccccaaaccccaaaaacccccccccccccaaaaacccccccccccaaaaccccccccccccccccccccccccaacaaaccccaaaaaaacaaaaacccccccccaccaaaciiiijjjjjjoooopppppkllcaaaaaaacccc\nabccaaccccccaaaaacaaaaacccccccccccccaaaaaacccccccccccccccccccccccccccccccccccccccaacccccccaaaacaaaaaaaaacccaaccccaaaaaciiiiinoooooooouuuupplllaaaaaacccccc\nabcaaacccccaaaaaacaaaaaacccccccccccaaaaaaaaccccccccaacaccccccccccccccccccccccccccccccccccccaccccccccccaaccaaaccccaaaaaciiinnnooooooouuuuuppplllaaacacccccc\nabaaaaaacccaaaaaacccaaaacccccccccccaaaaaaaaccccccccaaaaccccccccccccccccccccccccccccccccccccccccccccccccaaaaacaacaaaaaaiiinnnnntttoouuuuuupppllllcccccccccc\nabaaaaaaccccaaaaacccaaccccccccccacccccaaccccccccccaaaaaccccccccccccccccccccccccccccccccccccccccccccccccaaaaaaaacaaaaaaiiinnnnttttuuuuxxuuupppllllccccccccc\nabaaaaacccccaacaaccccccccccccccaaaccccaacccccaacccaaaaaacccccccccccccccccccccccccccccccccccccccccccccccccaaaaaccaaaaaaiiinnnttttxxuuxxyyuuppppllllcccccccc\nabaaaacccccccccccccccccccccaaacaaaccccccaaacaaaaccacaaaacccccccccccccccccccccccccccccccccccaacccccccccccaaaaaccccaaaccciinnntttxxxxxxxyyvvvqqqqqlllccccccc\nabaaaaaccccccccccccccccccccaaaaaaaaaacccaaaaaaacccccaaccccccccccccccccccccccccccccccccccccaaacccccccccccaacaaaccccccccciiinntttxxxxxxxyyvvvvvqqqqljjcccccc\nabccaaaccaccccccccaaacccccccaaaaaaaaaccccaaaaaacccccccccccccccccccccccccccccccaacccccccaaaaacaaccccccccccccaacccccccccchhinnnttxxxxxxyyyyyvvvvqqqjjjcccccc\nSbccccaaaacccccccaaaaaacccccccaaaaaccccccaaaaaaaaccccccccccccccccccccaaccccccaaaaccccccaaaaaaaacccccccccccccccccccccccchhhnnntttxxxxEzyyyyyvvvqqqjjjcccccc\nabccccaaaacccccccaaaaaaccccccaaaaaacccccaaaaaaaaaacccccccccccccccccccaaccccccaaaaccccccccaaaaacccccccccccccccccccccccccchhhnntttxxxyyyyyyyvvvvqqqjjjcccccc\nabcccaaaaaaccccccaaaaaacccccaaaaaaaccccaaaaaaaaaacccccccccccccccccaaaaaaaacccaaaacccccccaaaaaccccccccccccccccccccccccccchhmmmttxxxyyyyyyvvvvvqqqjjjdcccccc\nabcccaaaaaacccccccaaaaacccccaaacaaacaaaaaaaaaaccccccccccccaaacccccaaaaaaaaccccccccccccccaacaaacccccccaacaaacccccccccccchhhmmmtswwwyyyyyyvvvqqqqjjjjdddcccc\nabcccccaacccccccccaacaacccccccccccacaaaaaccaaaccccccccccaaaaacccccccaaaacccccccccccccccccccaaccccccccaaaaaacccccccccccchhhmmssswwwwwwyyywvrqqqjjjjdddccccc\nabcccccccccccccccccccccccccccccccccaaaaaccccaaccccccccacaaaaaacccccaaaaacccccccccccccccccccccccccccccaaaaaacccccccccccchhhmmssswwwwwwywywwrrqjjjjddddccccc\nabcccccccccccccccccccccccccccccccccaaaaaccccccccaaacaaacaaaaaacccc" <> ...

Solution

defmodule Day12 do
  defdelegate parse(input), to: __MODULE__.Input

  alias __MODULE__.{Mover, Queue}

  def part1(input) do
    input
    |> parse()
    |> find_end()
    |> elem(1)
  end

  def part2(input) do
    input
    |> parse()
    |> find_start()
    |> elem(1)
  end

  defp find_end(%{?S => start_xy, ?E => end_xy} = grid) do
    Mover.navigate(
      {
        %{start_xy => 0},
        Queue.add([], start_xy, 0)
      },
      grid,
      end_xy,
      :forward
    )
  end

  defp find_start(%{?E => end_xy} = grid) do
    Mover.navigate(
      {
        %{end_xy => 0},
        Queue.add([], end_xy, 0)
      },
      grid,
      ?a,
      :backward
    )
  end

  defmodule Mover do
    # Endpoint found. EXIT.
    def navigate({%{endpoint: endpoint}, _}, _, _, _), do: endpoint

    def navigate({seen, [{weight, xy} | queue]}, grid, the_end, dir) do
      new_xys(xy)
      |> Enum.reduce({seen, queue}, fn next_xy, acc ->
        move(
          acc,
          %{
            next_xy: next_xy,
            this: grid[xy],
            next: grid[next_xy],
            end: the_end,
            weight: weight,
            dir: dir
          }
        )
      end)
      |> navigate(grid, the_end, dir)
    end

    def move(acc, %{next: nil}), do: acc

    def move(acc, %{this: this, next: next, dir: :forward}) when next - this > 1,
      do: acc

    def move(acc, %{this: this, next: next, dir: :backward}) when this - next > 1,
      do: acc

    # EXIT: found the end
    def move({seen, queue}, %{end: the_end, next_xy: next_xy, next: next, weight: wt})
        when the_end in [next_xy, next],
        do: {Map.put(seen, :endpoint, {next_xy, wt + 1}), queue}

    def move({seen, queue}, %{next_xy: next_xy, weight: weight}) do
      if seen[next_xy] do
        {seen, queue}
      else
        {
          Map.put(seen, next_xy, weight + 1),
          Queue.add(queue, next_xy, weight + 1)
        }
      end
    end

    def new_xys({x, y}),
      do: [{x, y - 1}, {x, y + 1}, {x - 1, y}, {x + 1, y}]
  end

  defmodule Queue do
    def add([{lowest_weight, _} | _] = queue, xy, weight) when weight <= lowest_weight do
      [{weight, xy} | queue]
    end

    def add([head | tail], xy, weight) do
      [head | add(tail, xy, weight)]
    end

    def add([], xy, weight) do
      [{weight, xy}]
    end
  end

  defmodule Input do
    def parse(input) when is_binary(input) do
      input
      |> String.split("\n", trim: true)
      |> parse()
      |> put_start()
      |> put_end()
    end

    def parse(input) when is_list(input) do
      input
      |> Enum.with_index()
      |> Enum.reduce(%{}, fn {row, y}, acc ->
        row
        |> String.to_charlist()
        |> Enum.with_index()
        |> Enum.into(acc, fn {col, x} -> {{x, y}, col} end)
      end)
    end

    def put_start(grid) do
      start_xy = find_value(grid, ?S)
      grid |> Map.put(?S, start_xy) |> Map.put(start_xy, ?a)
    end

    def put_end(grid) do
      end_xy = find_value(grid, ?E)
      grid |> Map.put(?E, end_xy) |> Map.put(end_xy, ?z)
    end

    defp find_value(grid, value) do
      Enum.find_value(grid, fn
        {k, ^value} -> k
        _ -> nil
      end)
    end
  end
end
{:module, Day12, <<70, 79, 82, 49, 0, 0, 10, ...>>,
 {:module, Day12.Input, <<70, 79, 82, ...>>, {:find_value, 2}}}

What is the fewest steps required to move from your current position to the location that should get the best signal?

Your puzzle answer was 472.

Day12.part1(input)
472

What is the fewest steps required to move starting from any square with elevation a to the location that should get the best signal?

Your puzzle answer was 465.

Day12.part2(input)
465

Both parts of this puzzle are complete! They provide two gold stars: **

At this point, you should return to your Advent calendar and try another puzzle.

If you still want to see it, you can get your puzzle input.

Tests

ExUnit.start(auto_run: false)

defmodule Day12Test do
  use ExUnit.Case, async: false

  setup_all do
    [
      input: "Sabqponm\nabcryxxl\naccszExk\nacctuvwj\nabdefghi"
    ]
  end

  describe "part1" do
    test "returns expected value", %{input: input} do
      assert Day12.part1(input) == 31
    end
  end

  describe "part2" do
    test "returns expected value", %{input: input} do
      assert Day12.part2(input) == 29
    end
  end
end

ExUnit.run()
..
Finished in 0.00 seconds (0.00s async, 0.00s sync)
2 tests, 0 failures

Randomized with seed 665270
%{excluded: 0, failures: 0, skipped: 0, total: 2}

Benchmarking

# https://github.com/bencheeorg/benchee

Benchee.run(%{
  "Part 1" => fn -> Day12.part1(input) end,
  "Part 2" => fn -> Day12.part2(input) end
})

nil
Operating System: macOS
CPU Information: Apple M1
Number of Available Cores: 8
Available memory: 8 GB
Elixir 1.14.2
Erlang 25.1.2

Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 0 ns
reduction time: 0 ns
parallel: 1
inputs: none specified
Estimated total run time: 14 s

Benchmarking Part 1 ...
Benchmarking Part 2 ...

Name             ips        average  deviation         median         99th %
Part 2        157.00        6.37 ms    ±10.59%        6.26 ms        7.57 ms
Part 1        112.74        8.87 ms     ±3.53%        8.81 ms        9.89 ms

Comparison: 
Part 2        157.00
Part 1        112.74 - 1.39x slower +2.50 ms
nil