Powered by AppSignal & Oban Pro

Day 18 - Advent of Code 2022

lib/advent_of_code/2022/day-18.livemd

Day 18 - Advent of Code 2022

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

Links

Prompt

— Day 18: Boiling Boulders —

You and the elephants finally reach fresh air. You’ve emerged near the base of a large volcano that seems to be actively erupting! Fortunately, the lava seems to be flowing away from you and toward the ocean.

Bits of lava are still being ejected toward you, so you’re sheltering in the cavern exit a little longer. Outside the cave, you can see the lava landing in a pond and hear it loudly hissing as it solidifies.

Depending on the specific compounds in the lava and speed at which it cools, it might be forming obsidian! The cooling rate should be based on the surface area of the lava droplets, so you take a quick scan of a droplet as it flies past you (your puzzle input).

Because of how quickly the lava is moving, the scan isn’t very good; its resolution is quite low and, as a result, it approximates the shape of the lava droplet with 1x1x1 cubes on a 3D grid, each given as its x,y,z position.

To approximate the surface area, count the number of sides of each cube that are not immediately connected to another cube. So, if your scan were only two adjacent cubes like 1,1,1 and 2,1,1, each cube would have a single side covered and five sides exposed, a total surface area of 10 sides.

Here’s a larger example:

2,2,2
1,2,2
3,2,2
2,1,2
2,3,2
2,2,1
2,2,3
2,2,4
2,2,6
1,2,5
3,2,5
2,1,5
2,3,5

In the above example, after counting up all the sides that aren’t connected to another cube, the total surface area is 64.

What is the surface area of your scanned lava droplet?

To begin, get your puzzle input.

— Part Two —

Something seems off about your calculation. The cooling rate depends on exterior surface area, but your calculation also included the surface area of air pockets trapped in the lava droplet.

Instead, consider only cube sides that could be reached by the water and steam as the lava droplet tumbles into the pond. The steam will expand to reach as much as possible, completely displacing any air on the outside of the lava droplet but never expanding diagonally.

In the larger example above, exactly one cube of air is trapped within the lava droplet (at 2,2,5), so the exterior surface area of the lava droplet is 58.

What is the exterior surface area of your scanned lava droplet?

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()
"14,5,6\n12,4,14\n12,12,2\n11,19,10\n8,16,3\n13,2,8\n15,17,16\n18,11,17\n18,16,12\n4,8,10\n10,3,14\n17,13,6\n9,5,5\n13,15,16\n14,16,13\n16,14,17\n8,19,15\n6,8,5\n15,5,16\n19,12,8\n2,14,11\n8,11,2\n15,10,3\n6,16,9\n4,11,11\n5,11,18\n14,14,16\n17,14,5\n14,3,13\n17,18,7\n18,15,11\n18,11,4\n16,13,17\n10,2,11\n10,7,18\n11,2,12\n14,19,12\n14,7,3\n7,16,12\n12,15,17\n0,11,10\n15,11,3\n3,6,9\n16,6,5\n8,8,15\n18,7,11\n12,19,7\n16,15,9\n18,9,9\n1,10,13\n18,8,9\n18,14,11\n14,3,11\n15,16,14\n7,18,17\n9,12,2\n7,13,17\n8,2,9\n3,10,8\n8,13,18\n17,16,13\n17,17,10\n4,11,17\n16,16,14\n14,18,16\n4,9,17\n12,4,17\n9,18,9\n9,15,4\n16,18,12\n5,16,7\n8,16,17\n12,6,17\n9,1,12\n7,12,2\n7,8,18\n10,17,3\n5,13,17\n10,3,13\n8,3,16\n9,6,17\n3,15,15\n3,14,14\n18,16,8\n1,10,11\n5,8,16\n12,2,13\n14,7,4\n18,15,10\n10,3,10\n2,15,11\n11,6,2\n3,15,7\n2,7,9\n10,15,17\n13,12,3\n8,2,14\n6,16,7\n18,8,13\n6,5,12\n14,9,17\n5,10,6\n13,20,12\n8,3,14\n10,19,7\n9,3,15\n16,9,17\n8,16,6\n17,11,15\n17,15,6\n15,6,6\n12,8,20\n6,18,8\n17,16,12\n10,17,9\n10,4,17\n16,9,6\n13,10,3\n15,3,11\n8,4,13\n13,18,14\n10,20,11\n12,12,17\n3,8,9\n1,11,13\n15,17,7\n18,8,16\n16,3,12\n10,4,7\n7,4,4\n4,9,15\n4,6,12\n18,9,10\n5,6,6\n8,2,12\n10,16,4\n16,15,6\n15,4,10\n7,9,18\n5,16,9\n6,8,18\n6,10,2\n16,16,17\n10,12,2\n11,6,18\n15,2,8\n17,15,15\n4,17,10\n15,14,13\n7,3,10\n6,4,6\n19,9,14\n8,7,1\n10,10,21\n5,11,6\n18,6,11\n7,6,6\n16,8,18\n3,8,7\n2,14,8\n17,11,8\n7,19,9\n6,14,3\n10,13,2\n6,4,17\n16,11,5\n16,10,17\n17,11,10\n15,8,15\n20,9,11\n17,4,9\n10,11,18\n11,3,6\n13,18,4\n3,10,6\n18,16,9\n10,13,4\n12,16,18\n12,17,15\n5,13,15\n18,6,10\n12,19,12\n17,12,15\n10,15,5\n18,13,16\n8,9,18\n16,7,3\n8,2,11\n8,4,7\n18,10,14\n8,6,17\n17,7,17\n19,10,6\n16,13,16\n5,11,5\n2,7,10\n11,7,19\n7,12,18\n4,13,6\n3,14,7\n17,6,5\n8,8,18\n14,18,11\n6,16,13\n13,9,18\n10,5,4\n3,14,5\n15,5,15\n10,6,4\n7,14,3\n4,14,13\n8,5,6\n18,7,8\n14,4,17\n7,17,5\n8,5,17\n6,5,15\n5,4,12\n15,3,10\n5,4,9\n17,17,11\n12,4,6\n3,15,11\n9,6,19\n17,10,7\n8,11,20\n9,5,4\n6,3,14\n15,19,8\n17,12,5\n1,13,10\n9,18,5\n9,10,2\n17,10,5\n2,8,12\n13,18,15\n11,20,12\n3,11,9\n11,3,17\n11,5,17\n11,6,17\n6,6,16\n13,8,18\n9,4,14\n10,18,6\n9,16,3\n8,1,15\n16,14,8\n12,14,2\n6,3,10\n6,12,19\n14,17,15\n12,17,11\n15,8,5\n4,6,7\n10,8,2\n10,13,3\n4,13,17\n16,10,5\n15,4,13\n7,16,10\n12,12,19\n7,18,14\n14,15,9\n18,15,14\n2,11,13\n16,7,16\n15,16,6\n14,3,6\n11,15,19\n10,7,3\n19,9,8\n6,2,10\n14,3,14\n13,13,4\n12,11,2\n18,9,8\n10,18,17\n4,9,7\n7,9,19\n9,3,7\n9,20,10\n17,15,11\n18,8,10\n4,6,14\n10,10,18\n2,12,15\n7,6,5\n2,13,13\n5,4,8\n5,18,7\n10,10,2\n11,12,19\n3,17,10\n8,9,2\n19,15,11\n17,5,11\n6,16,16\n7,4,15\n9,2,7\n4,15,13\n17,13,15\n14,9,3\n20,10,10\n15,13,18\n2,7,7\n16,4,13\n8,4,9\n16,17,14\n6,2,9\n10,7,19\n13,11,18\n12,8,19\n18,11,14\n12,8,3\n4,16,13\n13,19,8\n5,17,11\n18,12,16\n17,10,12\n9,16,10\n4,4,8\n6,10,3\n14,6,3\n5,17,12\n7,18,7\n15,7,4\n1,10,12\n7,13,4\n9,13,1\n9,12,20\n8,18,12\n14,7,17\n4,7,16\n15,16,4\n4,10,14\n5,13,3\n4,15,7\n15,17,9\n17,5,12\n12,18,7\n12,19,8\n14,15,4\n5,4,16\n19,6,10\n7,16,17\n9,1,11\n16,9,18\n8,7,3\n16,9,3\n20,8,13\n11,16,3\n15,15,17\n5,6,12\n12,16,4\n16,8,4\n8,17,6\n8,5,12\n9,1,14\n7,7,17\n11,14,2\n1,12,11\n5,5,8\n19,8,11\n3,16,13\n13,15,17\n11,4,6\n12,16,16\n11,12,1\n10,18,12\n13,4,7\n15,4,11\n4,7,13\n4,5,11\n12,19,14\n9,10,18\n13,18,7\n13,6,18\n19,10,9\n8,18,13\n19,8,8\n6,9,19\n4,14,16\n9,3,10\n15,4,6\n7,17,7\n19,12,15\n7,10,19\n6,8,17\n12,3,8\n6,15,4\n8,8,4\n3,12,7\n8,10,3\n4,9,13\n16,15,4\n3,4,11\n4,12,9\n18,8,8\n10,12,20\n11,13,18\n1,11,10\n10,19,5\n5,15,15\n12,16,5\n9,3,12\n14,4,12\n4,14,6\n19,13,11\n15,10,18\n6,19,10\n16,3,8\n8,18,16\n8,7,18\n14,12,16\n15,8,19\n4,16,12\n19,11,14\n11,2,13\n11,18,10\n8,15,16\n11,3,9\n7,8,3\n6,19,9\n9,17,13\n19,11,9\n10,4,18\n19,15,10\n9,2,8\n5,5,14\n9,19,10\n13,14,3\n11,6,3\n15,17,15\n10,7,4\n14,3,9\n19,9,10\n10,8,3\n2,13,8\n16,4,9\n16,18,13\n4,16,9\n4,10,9\n3,16,9\n7,5,4\n9,8,18\n11,2,8\n12,2,12\n20,13,14\n8,15,7\n5,9,15\n7,13,18\n7,16,18\n4,13,7\n6,16,5\n4,14,15\n14,16,16\n18,10,13\n14,9,16\n6,4,7\n7,11,17\n4,8,7\n18,9,14\n10,9,18\n19,12,10\n3,7,13\n5,11,4\n7,5,15\n2,11,8\n18,9,16\n16,13,3\n11,15,2\n7,18,6\n11,16,18\n11,9,20\n16,17,10\n3,10,10\n20,12,10\n6,19,12\n4,16,11\n14,16,5\n5,4,10\n14,15,15\n11,5,4\n3,13,12\n14,15,5\n6,6,6\n8,15,17\n15,6,16\n4,15,9\n11,10,1\n6,13,5\n7,12,3\n3,12,6\n7,9,1\n8,18,8\n4,10,6\n6,5,17\n10,19,13\n5,10,17\n14,16,3\n3,7,12\n15,12,18\n2,12,8\n7,3,8\n8,15,18\n17,10,15\n13,16,5\n12,7,16\n11,4,5\n17,9,16\n4,10,5\n8,5,19\n2,6,11\n17,8,14\n14,10,3\n2,12,9\n6,10,19\n13,6,15\n16,6,9\n14,12,2\n15,4,5\n13,13,17\n11,17,16\n8,16,16\n5,15,7\n3,14,8\n6,15,3\n4,6,10\n16,4,12\n9,11,18\n6,16,15\n13,2,6\n1" <> ...

Solution

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

  def part1(input) do
    grid = input |> parse() |> MapSet.new()

    grid
    |> Enum.map(fn xyz ->
      6 - neighbor_count(xyz, grid)
    end)
    |> Enum.sum()
  end

  def part2(input) do
    grid = input |> parse() |> MapSet.new()
    outside_spaces = empty_outside_spaces(grid)

    grid
    |> Enum.map(fn xyz ->
      neighbor_count(xyz, outside_spaces)
    end)
    |> Enum.sum()
  end

  def neighbors({x, y, z}) do
    [
      {1, 0, 0},
      {-1, 0, 0},
      {0, 1, 0},
      {0, -1, 0},
      {0, 0, 1},
      {0, 0, -1}
    ]
    |> Enum.map(fn {a, b, c} -> {a + x, b + y, c + z} end)
  end

  def neighbor_count(xyz, set) do
    Enum.count(neighbors(xyz), &amp;MapSet.member?(set, &amp;1))
  end

  def empty_outside_spaces(grid) do
    {min, max} = min_max(grid)
    min = min - 1
    max = max + 1

    find_empty_spaces([{min, min, min}], MapSet.new(), grid, min, max)
  end

  def find_empty_spaces([], seen, _, _, _), do: seen

  def find_empty_spaces([next | queue], seen, grid, min, max) do
    next
    |> neighbors()
    |> Enum.reduce({queue, seen}, fn {x, y, z} = xyz, {queue, seen} ->
      cond do
        MapSet.member?(grid, xyz) -> {queue, seen}
        MapSet.member?(seen, xyz) -> {queue, seen}
        Enum.any?([x, y, z], &amp;(&amp;1 < min or &amp;1 > max)) -> {queue, seen}
        true -> {[xyz | queue], MapSet.put(seen, xyz)}
      end
    end)
    |> then(fn {queue, seen} -> find_empty_spaces(queue, seen, grid, min, max) end)
  end

  def min_max(grid) do
    {min, max} = min_max_xyz(grid)

    {
      min |> Tuple.to_list() |> Enum.min(),
      max |> Tuple.to_list() |> Enum.max()
    }
  end

  def min_max_xyz(grid) do
    {min_x, max_x} = grid |> Enum.map(&amp;elem(&amp;1, 0)) |> Enum.min_max()
    {min_y, max_y} = grid |> Enum.map(&amp;elem(&amp;1, 1)) |> Enum.min_max()
    {min_z, max_z} = grid |> Enum.map(&amp;elem(&amp;1, 2)) |> Enum.min_max()

    {
      {min_x, min_y, min_z},
      {max_x, max_y, max_z}
    }
  end

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

    def parse(input) when is_list(input) do
      Stream.map(input, &amp;parse_line/1)
    end

    def parse_line(line) do
      line
      |> String.split(",", trim: true)
      |> Enum.map(&amp;String.to_integer/1)
      |> List.to_tuple()
    end
  end
end
{:module, Day18, <<70, 79, 82, 49, 0, 0, 26, ...>>,
 {:module, Day18.Input, <<70, 79, 82, ...>>, {:parse_line, 1}}}

What is the surface area of your scanned lava droplet?

Your puzzle answer was 4636.

Day18.part1(input)
4636

What is the exterior surface area of your scanned lava droplet?

Your puzzle answer was 2572.

Day18.part2(input)
2572

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 Day18Test do
  use ExUnit.Case, async: false

  setup_all do
    [
      input:
        "2,2,2\n1,2,2\n3,2,2\n2,1,2\n2,3,2\n2,2,1\n2,2,3\n2,2,4\n2,2,6\n1,2,5\n3,2,5\n2,1,5\n2,3,5"
    ]
  end

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

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

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

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

Benchmarking

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

Benchee.run(%{
  "Part 1" => fn -> Day18.part1(input) end,
  "Part 2" => fn -> Day18.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 1        345.05        2.90 ms     ±2.73%        2.89 ms        3.13 ms
Part 2         65.00       15.38 ms     ±2.46%       15.36 ms       16.13 ms

Comparison: 
Part 1        345.05
Part 2         65.00 - 5.31x slower +12.49 ms
nil