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

Day 9: Smoke Basin

day9/Day9.livemd

Day 9: Smoke Basin

Dependencies

Mix.install(
  [
    {:aoc_common, github: "rob-brown/AdventOfCode2021"},
    {:kino, "~> 0.4.0"}
  ],
  force: false
)
input = Kino.Input.textarea("Please paste your input file:")

Part 1

These caves seem to be lava tubes. Parts are even still volcanically active; small hydrothermal vents release smoke into the caves that slowly settles like rain.

If you can model how the smoke flows through the caves, you might be able to avoid it and be that much safer. The submarine generates a heightmap of the floor of the nearby caves for you (your puzzle input).

Smoke flows to the lowest point of the area it’s in. For example, consider the following heightmap:

2199943210
3987894921
9856789892
8767896789
9899965678

Each number corresponds to the height of a particular location, where 9 is the highest and 0 is the lowest a location can be.

Your first goal is to find the low points - the locations that are lower than any of its adjacent locations. Most locations have four adjacent locations (up, down, left, and right); locations on the edge or corner of the map have three or two adjacent locations, respectively. (Diagonal locations do not count as adjacent.)

In the above example, there are four low points, all highlighted: two are in the first row (a 1 and a 0), one is in the third row (a 5), and one is in the bottom row (also a 5). All other locations on the heightmap have some lower adjacent location, and so are not low points.

The risk level of a low point is 1 plus its height. In the above example, the risk levels of the low points are 2, 1, 6, and 6. The sum of the risk levels of all low points in the heightmap is therefore 15.

Find all of the low points on your heightmap. What is the sum of the risk levels of all low points on your heightmap?

defmodule Part1 do
  use AocCommon

  def run(input) do
    input
    |> String.split("\n", trim: true)
    |> create_grid()
    |> find_low_points()
    |> Enum.map(&(&1 + 1))
    |> Enum.sum()
  end

  defp create_grid(lines) do
    for {line, y} <- Enum.with_index(lines) do
      for {c, x} <- line |> String.graphemes() |> Enum.with_index() do
        {{x, y}, String.to_integer(c)}
      end
    end
    |> List.flatten()
    |> Map.new()
  end

  def find_low_points(grid) do
    {max_x, max_y} = grid |> Map.keys() |> Enum.max()

    for x <- 0..max_x, y <- 0..max_y do
      point = {x, y}
      height = Map.get(grid, point, 9)

      neighbors =
        point
        |> adjactent_points()
        |> Enum.map(&amp;Map.get(grid, &amp;1, 9))

      if Enum.all?(neighbors, &amp;(&amp;1 > height)) do
        height
      else
        nil
      end
    end
    |> Enum.reject(&amp;is_nil/1)
  end

  defp adjactent_points({x, y}) do
    [
      {x + 1, y},
      {x - 1, y},
      {x, y + 1},
      {x, y - 1}
    ]
  end
end

input
|> Kino.Input.read()
|> Part1.run()

Part 2

Next, you need to find the largest basins so you know what areas are most important to avoid.

A basin is all locations that eventually flow downward to a single low point. Therefore, every low point has a basin, although some basins are very small. Locations of height 9 do not count as being in any basin, and all other locations will always be part of exactly one basin.

The size of a basin is the number of locations within the basin, including the low point. The example above has four basins.

The top-left basin, size 3:

2199943210
3987894921
9856789892
8767896789
9899965678

The top-right basin, size 9:

2199943210
3987894921
9856789892
8767896789
9899965678

The middle basin, size 14:

2199943210
3987894921
9856789892
8767896789
9899965678

The bottom-right basin, size 9:

2199943210
3987894921
9856789892
8767896789
9899965678

Find the three largest basins and multiply their sizes together. In the above example, this is 9 * 14 * 9 = 1134.

What do you get if you multiply together the sizes of the three largest basins?

defmodule Part2 do
  use AocCommon

  def run(input) do
    grid =
      input
      |> String.split("\n", trim: true)
      |> create_grid()

    grid
    |> find_low_points()
    |> Enum.map(&amp;find_basin(&amp;1, grid))
    |> Enum.map(&amp;Enum.count/1)
    |> Enum.sort(:desc)
    |> Enum.take(3)
    |> then(fn [x, y, z] -> x * y * z end)
  end

  defp create_grid(lines) do
    for {line, y} <- Enum.with_index(lines) do
      for {c, x} <- line |> String.graphemes() |> Enum.with_index() do
        {{x, y}, String.to_integer(c)}
      end
    end
    |> List.flatten()
    |> Map.new()
  end

  def find_low_points(grid) do
    {max_x, max_y} = grid |> Map.keys() |> Enum.max()

    for x <- 0..max_x, y <- 0..max_y do
      point = {x, y}
      height = Map.get(grid, point, 9)

      neighbors =
        point
        |> adjactent_points()
        |> Enum.map(&amp;Map.get(grid, &amp;1, 9))

      if Enum.all?(neighbors, &amp;(&amp;1 > height)) do
        point
      else
        nil
      end
    end
    |> Enum.reject(&amp;is_nil/1)
  end

  defp adjactent_points({x, y}) do
    [
      {x + 1, y},
      {x - 1, y},
      {x, y + 1},
      {x, y - 1}
    ]
  end

  defp find_basin(point, grid, set \\ MapSet.new()) do
    height = Map.get(grid, point, 9)

    if height == 9 do
      set
    else
      set = MapSet.put(set, point)

      point
      |> adjactent_points()
      |> Enum.reject(&amp;(&amp;1 in set))
      |> Enum.reduce(set, fn neighbor, set ->
        find_basin(neighbor, grid, set)
      end)
    end
  end
end

input
|> Kino.Input.read()
|> Part2.run()

Test

ExUnit.start(autorun: false)

defmodule Day9Tests do
  use ExUnit.Case, async: true

  setup do
    input = """
    2199943210
    3987894921
    9856789892
    8767896789
    9899965678
    """

    [input: input]
  end

  test "part1", %{input: input} do
    assert Part1.run(input) == 15
  end

  test "part2", %{input: input} do
    assert Part2.run(input) == 1134
  end
end

ExUnit.run()