Powered by AppSignal & Oban Pro

Day 9: Smoke Basin

2021/day_09_smoke_basin.livemd

Day 9: Smoke Basin

Mix.install([:kino])

input = Kino.Input.textarea("Please paste your input:")

Setup

inputs =
  input
  |> Kino.Input.read()
  |> String.split("\n", trim: true)
  |> Enum.map(fn line ->
    line |> String.split("", trim: true) |> Enum.map(&String.to_integer/1)
  end)

Part 1

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

Solve: Part 1

defmodule Day9.Part1 do
  def size(heightmap) do
    width = heightmap |> List.first() |> Enum.count()
    height = heightmap |> Enum.count()

    {width, height}
  end

  def is_low_point?(heightmap, x, y, {width, height}) do
    point_value = get_value(heightmap, x, y)

    for {dx, dy} <- [{-1, 0}, {1, 0}, {0, -1}, {0, 1}],
        x + dx > -1,
        x + dx < width,
        y + dy > -1,
        y + dy < height do
      get_value(heightmap, x + dx, y + dy)
    end
    |> Enum.all?(&(&1 > point_value))
  end

  def risk_level(heightmap, x, y) do
    get_value(heightmap, x, y) + 1
  end

  defp get_value(heightmap, x, y) do
    heightmap |> Enum.at(y) |> Enum.at(x)
  end
end

{width, height} = size = Day9.Part1.size(inputs)

for x <- 0..(width - 1), y <- 0..(height - 1), Day9.Part1.is_low_point?(inputs, x, y, size) do
  Day9.Part1.risk_level(inputs, x, y)
end
|> Enum.sum()

Part 2

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

Solve: Part 2

defmodule Day9.Part2 do
  def size(heightmap) do
    width = heightmap |> List.first() |> Enum.count()
    height = heightmap |> Enum.count()

    {width, height}
  end

  def is_low_point?(heightmap, {x, y}, size) do
    point_value = get_value(heightmap, {x, y})

    get_adjacent_points({x, y}, size)
    |> Enum.map(&get_value(heightmap, &1))
    |> Enum.all?(&(&1 > point_value))
  end

  def risk_level(heightmap, {x, y}) do
    get_value(heightmap, {x, y}) + 1
  end

  def calc_basin_size(heightmap, point, size) do
    finished_set = MapSet.new()
    not_finished_list = [point]

    do_calc_basin_size(heightmap, finished_set, not_finished_list, size)
  end

  def do_calc_basin_size(
        heightmap,
        finished_set,
        [current_point | not_finished_list],
        size
      ) do
    adjacent_points =
      get_adjacent_points(current_point, size)
      |> Enum.reject(fn point ->
        MapSet.member?(finished_set, point) || get_value(heightmap, point) == 9
      end)

    do_calc_basin_size(
      heightmap,
      MapSet.put(finished_set, current_point),
      Enum.uniq(not_finished_list ++ adjacent_points),
      size
    )
  end

  def do_calc_basin_size(_heightmap, finished_set, [], _size) do
    MapSet.size(finished_set)
  end

  defp get_adjacent_points({x, y}, size) do
    for {dx, dy} <- [{-1, 0}, {1, 0}, {0, -1}, {0, 1}], is_valid_point?({x + dx, y + dy}, size) do
      {x + dx, y + dy}
    end
  end

  defp is_valid_point?({x, y}, {width, height}) do
    x > -1 and x < width and y > -1 and y < height
  end

  defp get_value(heightmap, {x, y}) do
    heightmap |> Enum.at(y) |> Enum.at(x)
  end
end

alias Day9.Part2

{width, height} = size = Part2.size(inputs)

for x <- 0..(width - 1), y <- 0..(height - 1), Part2.is_low_point?(inputs, {x, y}, size) do
  Part2.calc_basin_size(inputs, {x, y}, size)
end
|> Enum.sort(:desc)
|> Enum.take(3)
|> Enum.reduce(1, &(&1 * &2))