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

Advent of Code 2021 - Day 9

advent-of-code/2021/day09.livemd

Advent of Code 2021 - Day 9

Mix.install([
  {:kino, "~> 0.6.1"},
  {:vega_lite, "~> 0.1.4"},
  {:kino_vega_lite, "~> 0.1.1"}
])

alias VegaLite, as: Vl

Instructions

https://adventofcode.com/2021/day/9

Test Input

test_input = """
2199943210
3987894921
9856789892
8767896789
9899965678
"""
test_input =
  test_input
  |> String.split("\n", trim: true)

Puzzle Input

Paste your own input from Advent of Code or copy from sdball 2021 Advent of Code Day 9 input

puzzle_input = Kino.Input.textarea("Please paste your day 9 input:")
puzzle_input =
  puzzle_input
  |> Kino.Input.read()
  |> String.split("\n", trim: true)

Seafloor Mapping - Test Input

First we construct a map (literally a Map) of the seafloor readings.

IO.puts(test_input |> Enum.join("\n"))
Enum.zip(0..9, ?0..?9)
charlists = Enum.map(test_input, &to_charlist/1)

seafloor_map =
  for {line, row} <- Enum.with_index(charlists),
      {depth, column} <- Enum.with_index(line),
      into: %{},
      do: {{column, row}, depth - ?0}
{
  seafloor_map[{0, 0}],
  seafloor_map[{1, 1}],
  seafloor_map[{1, 2}]
}
seafloor_graph =
  seafloor_map
  |> Enum.map(fn {{x, y}, depth} ->
    %{"x" => x, "y" => y, "depth" => depth}
  end)
Vl.new(width: 500, height: 300)
|> Vl.data_from_values(seafloor_graph)
|> Vl.mark(:rect, tooltip: [content: "data"])
|> Vl.encode_field(:x, "x", type: :ordinal)
|> Vl.encode_field(:y, "y", type: :ordinal)
|> Vl.encode(:color,
  field: :depth,
  type: :quantitative,
  scale: [
    range: [
      "#000",
      "#116",
      "#227",
      "#338",
      "#449",
      "#55a",
      "#66b",
      "#77c",
      "#88c",
      "#eed"
    ]
  ],
  legend: true
)

Cool right? We’ve generated a heatmap of the ocean floor from the input data!

In the data 0 is the lowest possible depth which is why I’m using a custom color scheme for the scale.

Seafloor Mapping - Puzzle Input

charlists = Enum.map(puzzle_input, &amp;to_charlist/1)

seafloor_map =
  for {line, row} <- Enum.with_index(charlists),
      {reading, column} <- Enum.with_index(line),
      into: %{},
      do: {{column, row}, reading - ?0}
seafloor_graph =
  seafloor_map
  |> Enum.map(fn {{x, y}, depth} ->
    %{"x" => x, "y" => y, "depth" => depth}
  end)

Graphing the puzzle seafloor

Vl.new(width: 600, height: 600)
|> Vl.data_from_values(seafloor_graph)
|> Vl.mark(:rect, tooltip: [content: "data"])
|> Vl.encode_field(:x, "x", type: :ordinal, axis: [labels: false])
|> Vl.encode_field(:y, "y", type: :ordinal, axis: [labels: false])
|> Vl.encode(:color,
  field: :depth,
  type: :quantitative,
  scale: [
    range: [
      "#000",
      "#116",
      "#227",
      "#338",
      "#449",
      "#55a",
      "#66b",
      "#77c",
      "#88c",
      "#eed"
    ]
  ],
  legend: true
)

Cool!

Part 1 - Test Input

seafloor_map =
  for {line, row} <- Enum.with_index(test_input),
      {depth, column} <- Enum.with_index(String.to_charlist(line)),
      into: %{},
      do: {{column, row}, depth - ?0}

low_points =
  seafloor_map
  |> Enum.filter(fn {{row, column}, depth} ->
    up = seafloor_map[{row - 1, column}]
    down = seafloor_map[{row + 1, column}]
    left = seafloor_map[{row, column - 1}]
    right = seafloor_map[{row, column + 1}]

    depth < up and depth < down and depth < left and depth < right
  end)

risk =
  Enum.reduce(low_points, 0, fn {_point, depth}, risk ->
    risk + depth + 1
  end)

Part 1 - Puzzle Input

seafloor_map =
  for {line, row} <- Enum.with_index(puzzle_input),
      {depth, column} <- Enum.with_index(String.to_charlist(line)),
      into: %{},
      do: {{column, row}, depth - ?0}

low_points =
  seafloor_map
  |> Enum.filter(fn {{row, column}, depth} ->
    up = seafloor_map[{row - 1, column}]
    down = seafloor_map[{row + 1, column}]
    left = seafloor_map[{row, column - 1}]
    right = seafloor_map[{row, column + 1}]

    depth < up and depth < down and depth < left and depth < right
  end)

risk =
  Enum.reduce(low_points, 0, fn {_point, depth}, risk ->
    risk + depth + 1
  end)
seen =
  MapSet.new()
  |> MapSet.put({0, 1})
  |> MapSet.put({0, 1})
  |> MapSet.put({0, 1})
  |> MapSet.put({2, 1})
  |> MapSet.put({2, 2})

MapSet.size(seen)

Scanner Module

defmodule Scanner do
  def seafloor_map(input) do
    for {line, row} <- Enum.with_index(input),
        {depth, column} <- Enum.with_index(String.to_charlist(line)),
        into: %{},
        do: {{column, row}, depth - ?0}
  end

  def low_points(seafloor_map) do
    seafloor_map
    |> Enum.filter(fn {{row, column}, depth} ->
      up = seafloor_map[{row - 1, column}]
      down = seafloor_map[{row + 1, column}]
      left = seafloor_map[{row, column - 1}]
      right = seafloor_map[{row, column + 1}]
      depth < up and depth < down and depth < left and depth < right
    end)
  end

  def basins(seafloor_map) do
    seafloor_map
    |> low_points()
    |> Enum.map(fn {point, _depth} ->
      basin(MapSet.new(), point, seafloor_map)
    end)
  end

  def basin(seen, point, seafloor_map) do
    depth = seafloor_map[point]

    if point in seen or depth == 9 or depth == nil do
      seen
    else
      {row, column} = point
      up = {row - 1, column}
      down = {row + 1, column}
      left = {row, column - 1}
      right = {row, column + 1}

      seen
      |> MapSet.put(point)
      |> basin(up, seafloor_map)
      |> basin(down, seafloor_map)
      |> basin(left, seafloor_map)
      |> basin(right, seafloor_map)
    end
  end
end

Part 2 - Test Input

seafloor_map = Scanner.seafloor_map(test_input)

Scanner.basins(seafloor_map)
|> Enum.map(&amp;MapSet.size/1)
|> Enum.sort(:desc)
|> Enum.take(3)
|> Enum.product()

Part 2 - Puzzle Input

seafloor_map = Scanner.seafloor_map(puzzle_input)

Scanner.basins(seafloor_map)
|> Enum.map(&amp;MapSet.size/1)
|> Enum.sort(:desc)
|> Enum.take(3)
|> Enum.product()