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

Advent of Code 2023 - Day 10

2023/elixir/livebooks/day10.livemd

Advent of Code 2023 - Day 10

Mix.install([
  {:kino_aoc, "~> 0.1"}
])

Introduction

— Day 10: Pipe Maze —

You use the hang glider to ride the hot air from Desert Island all the way up to the floating metal island. This island is surprisingly cold and there definitely aren’t any thermals to glide on, so you leave your hang glider behind.

You wander around for a while, but you don’t find any people or animals. However, you do occasionally find signposts labeled “Hot Springs“ pointing in a seemingly consistent direction; maybe you can find someone at the hot springs and ask them where the desert-machine parts are made.

The landscape here is alien; even the flowers and trees are made of metal. As you stop to admire some metal grass, you notice something metallic scurry away in your peripheral vision and jump into a big pipe! It didn’t look like any animal you’ve ever seen; if you want a better look, you’ll need to get ahead of it.

Scanning the area, you discover that the entire field you’re standing on is densely packed with pipes; it was hard to tell at first because they’re the same metallic silver color as the “ground”. You make a quick sketch of all of the surface pipes you can see (your puzzle input).

The pipes are arranged in a two-dimensional grid of tiles:

  • | is a vertical pipe connecting north and south.
  • - is a horizontal pipe connecting east and west.
  • L is a 90-degree bend connecting north and east.
  • J is a 90-degree bend connecting north and west.
  • 7 is a 90-degree bend connecting south and west.
  • F is a 90-degree bend connecting south and east.
  • . is ground; there is no pipe in this tile.
  • S is the starting position of the animal; there is a pipe on this tile, but your sketch doesn’t show what shape the pipe has.

Based on the acoustics of the animal’s scurrying, you’re confident the pipe that contains the animal is one large, continuous loop.

For example, here is a square loop of pipe:

.....
.F-7.
.|.|.
.L-J.
.....

If the animal had entered this loop in the northwest corner, the sketch would instead look like this:

.....
.S-7.
.|.|.
.L-J.
.....

In the above diagram, the S tile is still a 90-degree F bend: you can tell because of how the adjacent pipes connect to it.

Unfortunately, there are also many pipes that aren’t connected to the loop! This sketch shows the same loop as above:

-L|F7
7S-7|
L|7||
-L-J|
L|-JF

In the above diagram, you can still figure out which pipes form the main loop: they’re the ones connected to S, pipes those pipes connect to, pipes those pipes connect to, and so on. Every pipe in the main loop connects to its two neighbors (including S, which will have exactly two pipes connecting to it, and which is assumed to connect back to those two pipes).

Here is a sketch that contains a slightly more complex main loop:

..F7.
.FJ|.
SJ.L7
|F--J
LJ...

Here’s the same example sketch with the extra, non-main-loop pipe tiles also shown:

7-F7-
.FJ|7
SJLL7
|F--J
LJ.LJ

If you want to get out ahead of the animal, you should find the tile in the loop that is farthest from the starting position. Because the animal is in the pipe, it doesn’t make sense to measure this by direct distance. Instead, you need to find the tile that would take the longest number of steps along the loop to reach from the starting point - regardless of which way around the loop the animal went.

In the first example with the square loop:

.....
.S-7.
.|.|.
.L-J.
.....

You can count the distance each tile in the loop is from the starting point like this:

.....
.012.
.1.3.
.234.
.....

In this example, the farthest point from the start is 4 steps away.

Here’s the more complex loop again:

..F7.
.FJ|.
SJ.L7
|F--J
LJ...

Here are the distances for each tile on that loop:

..45.
.236.
01.78
14567
23...

Find the single giant loop starting at S. How many steps along the loop does it take to get from the starting position to the point farthest from the starting position?

— Part Two —

You quickly reach the farthest point of the loop, but the animal never emerges. Maybe its nest is within the area enclosed by the loop?

To determine whether it’s even worth taking the time to search for such a nest, you should calculate how many tiles are contained within the loop. For example:

...........
.S-------7.
.|F-----7|.
.||.....||.
.||.....||.
.|L-7.F-J|.
.|..|.|..|.
.L--J.L--J.
...........

The above loop encloses merely four tiles - the two pairs of . in the southwest and southeast (marked I below). The middle . tiles (marked O below) are not in the loop. Here is the same loop again with those regions marked:

...........
.S-------7.
.|F-----7|.
.||OOOOO||.
.||OOOOO||.
.|L-7OF-J|.
.|II|O|II|.
.L--JOL--J.
.....O.....

In fact, there doesn’t even need to be a full tile path to the outside for tiles to count as outside the loop - squeezing between pipes is also allowed! Here, I is still within the loop and O is still outside the loop:

..........
.S------7.
.|F----7|.
.||OOOO||.
.||OOOO||.
.|L-7F-J|.
.|II||II|.
.L--JL--J.
..........

In both of the above examples, 4 tiles are enclosed by the loop.

Here’s a larger example:

.F----7F7F7F7F-7....
.|F--7||||||||FJ....
.||.FJ||||||||L7....
FJL7L7LJLJ||LJ.L-7..
L--J.L7...LJS7F-7L7.
....F-J..F7FJ|L7L7L7
....L7.F7||L7|.L7L7|
.....|FJLJ|FJ|F7|.LJ
....FJL-7.||.||||...
....L---J.LJ.LJLJ...

The above sketch has many random bits of ground, some of which are in the loop (I) and some of which are outside it (O):

OF----7F7F7F7F-7OOOO
O|F--7||||||||FJOOOO
O||OFJ||||||||L7OOOO
FJL7L7LJLJ||LJIL-7OO
L--JOL7IIILJS7F-7L7O
OOOOF-JIIF7FJ|L7L7L7
OOOOL7IF7||L7|IL7L7|
OOOOO|FJLJ|FJ|F7|OLJ
OOOOFJL-7O||O||||OOO
OOOOL---JOLJOLJLJOOO

In this larger example, 8 tiles are enclosed by the loop.

Any tile that isn’t part of the main loop can count as being enclosed by the loop. Here’s another example with many bits of junk pipe lying around that aren’t connected to the main loop at all:

FF7FSF7F7F7F7F7F---7
L|LJ||||||||||||F--J
FL-7LJLJ||||||LJL-77
F--JF--7||LJLJ7F7FJ-
L---JF-JLJ.||-FJLJJ7
|F|F-JF---7F7-L7L|7|
|FFJF7L7F-JF7|JL---7
7-L-JL7||F7|L7F-7F7|
L.L7LFJ|||||FJL7||LJ
L7JLJL-JLJLJL--JLJ.L

Here are just the tiles that are enclosed by the loop marked with I:

FF7FSF7F7F7F7F7F---7
L|LJ||||||||||||F--J
FL-7LJLJ||||||LJL-77
F--JF--7||LJLJIF7FJ-
L---JF-JLJIIIIFJLJJ7
|F|F-JF---7IIIL7L|7|
|FFJF7L7F-JF7IIL---7
7-L-JL7||F7|L7F-7F7|
L.L7LFJ|||||FJL7||LJ
L7JLJL-JLJLJL--JLJ.L

In this last example, 10 tiles are enclosed by the loop.

Figure out whether you have time to search for the nest by calculating the area within the loop. How many tiles are enclosed by the loop?

Puzzle

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2023", "10", System.fetch_env!("LB_AOC_SESSION"))
IO.puts(puzzle_input)

Tools

Code - Tools

defmodule Tools do
  def get_size(matrix) do
    size_x = matrix |> hd() |> length()
    size_y = matrix |> length()

    {size_x, size_y}
  end

  def get_value(matrix, {x, y}) do
    {size_x, size_y} = get_size(matrix)

    cond do
      x < 0 or x >= size_x ->
        "."

      y < 0 or y >= size_y ->
        "."

      true ->
        matrix
        |> Enum.at(y, [])
        |> Enum.at(x, ".")
    end
  end

  def valid?(matrix, {x, y}) do
    {size_x, size_y} = get_size(matrix)

    x >= 0 and x < size_x and y >= 0 and y < size_y
  end
end

Part One

Code - Part 1

defmodule PartOne do
  def solve(input) do
    IO.puts("--- Part One ---")
    IO.puts("Result: #{run(input)}")
  end

  def run(input) do
    matrix =
      input
      |> String.split("\n", trim: true)
      |> Enum.map(&amp;String.codepoints(&amp;1))

    {size_x, size_y} = Tools.get_size(matrix)

    [start] =
      for y <- 0..(size_y - 1),
          x <- 0..(size_x - 1),
          Tools.get_value(matrix, {x, y}) == "S",
          do: {x, y}

    loop(matrix, start)
    |> div(2)
  end

  def loop(matrix, start), do: loop(matrix, start, start, start, "S", 0)
  def loop(_matrix, start, _prev, curr, _pipe, step) when start == curr and step != 0, do: step

  def loop(matrix, start, prev, curr, pipe, step) do
    curr
    |> pipes_to_points()
    |> Map.get(pipe)
    |> Enum.reduce_while(step, fn next, acc ->
      pipe = Tools.get_value(matrix, next)
      pipes = points_to_pipes(curr) |> Map.get(next)
      condition = pipe in pipes and prev != next

      case condition do
        true -> {:halt, loop(matrix, start, curr, next, pipe, step + 1)}
        false -> {:cont, acc}
      end
    end)
  end

  defp pipes_to_points({x, y}) do
    %{
      "|" => [{x, y - 1}, {x, y + 1}],
      "-" => [{x - 1, y}, {x + 1, y}],
      "L" => [{x, y - 1}, {x + 1, y}],
      "J" => [{x, y - 1}, {x - 1, y}],
      "7" => [{x - 1, y}, {x, y + 1}],
      "F" => [{x + 1, y}, {x, y + 1}],
      "S" => [{x, y - 1}, {x + 1, y}, {x, y + 1}, {x - 1, y}]
    }
  end

  defp points_to_pipes({x, y}) do
    %{
      {x, y - 1} => ["|", "7", "F", "S"],
      {x + 1, y} => ["-", "7", "J", "S"],
      {x, y + 1} => ["|", "L", "J", "S"],
      {x - 1, y} => ["-", "L", "F", "S"]
    }
  end
end

Tests - Part 1

ExUnit.start(autorun: false)

defmodule PartOneTest do
  use ExUnit.Case, async: true
  import PartOne

  @input1 """
  .....
  .S-7.
  .|.|.
  .L-J.
  .....
  """
  @expected1 4

  @input2 """
  7-F7-
  .FJ|7
  SJLL7
  |F--J
  LJ.LJ
  """
  @expected2 8

  test "part one" do
    assert run(@input1) == @expected1
    assert run(@input2) == @expected2
  end
end

ExUnit.run()

Solution - Part 1

PartOne.solve(puzzle_input)

Part Two

In this part, I expand the matrix and I add a edge of 1 unit, the final expanded size is:

{size_x * 2 + 2, size_y * 2 + 2}

Then, I find all tiles not in the loop and I replace it with O and I replace the loop tiles with O too. And the end, I colapse the matrix to original size, and I count all tiles that don’t contain the O

Code - Part 2

defmodule PartTwo do
  def solve(input) do
    IO.puts("--- Part Two ---")
    IO.puts("Result: #{run(input)}")
  end

  def run(input) do
    matrix =
      input
      |> String.split("\n", trim: true)
      |> Enum.map(&amp;String.codepoints(&amp;1))

    # Expanded matrix
    matrix =
      matrix
      |> expand_matrix()

    {size_x, size_y} = Tools.get_size(matrix)

    [start] =
      for y <- 0..(size_y - 1),
          x <- 0..(size_x - 1),
          Tools.get_value(matrix, {x, y}) == "S",
          do: {x, y}

    points = loop(matrix, start) |> MapSet.new()

    zeroes = out_of_loop(matrix, points, {0, 0})

    out =
      zeroes
      |> Agent.get(fn set -> set end)
      |> MapSet.new()

    Agent.stop(zeroes)

    # Add "O" (Out) in the matrix
    matrix =
      for y <- 0..(size_y - 1) do
        for x <- 0..(size_x - 1) do
          case MapSet.member?(out, {x, y}) or MapSet.member?(points, {x, y}) do
            true -> "O"
            false -> Tools.get_value(matrix, {x, y})
          end
        end
      end

    # Normal matrix
    matrix =
      matrix
      |> colapse_matrix()

    {size_x, size_y} = Tools.get_size(matrix)

    for y <- 0..(size_y - 1),
        x <- 0..(size_x - 1),
        Tools.get_value(matrix, {x, y}) != "O",
        reduce: 0 do
      acc ->
        acc + 1
    end
  end

  # Expand the matrix, e.g. 10x10 to 22x22 (size * 2 + 2)
  def expand_matrix(matrix) do
    {size_x, size_y} = Tools.get_size(matrix)
    size_x = size_x * 2 + 2
    size_y = size_y * 2 + 2

    for y <- 0..(size_y - 1) do
      for x <- 0..(size_x - 1) do
        cond do
          x == 0 or x == size_x * 2 + 1 ->
            "."

          y == 0 or y == size_y * 2 + 1 ->
            "."

          rem(x, 2) == 0 and rem(y, 2) == 0 ->
            "."

          rem(x, 2) == 1 and rem(y, 2) == 1 ->
            Tools.get_value(matrix, {div(x, 2), div(y, 2)})

          rem(x, 2) == 1 ->
            up = Tools.get_value(matrix, {div(x, 2), div(y - 1, 2)})
            down = Tools.get_value(matrix, {div(x, 2), div(y + 1, 2)})
            condition = up in ["|", "7", "F", "S"] and down in ["|", "L", "J", "S"]

            case condition do
              true -> "|"
              false -> "."
            end

          rem(y, 2) == 1 ->
            left = Tools.get_value(matrix, {div(x - 1, 2), div(y, 2)})
            right = Tools.get_value(matrix, {div(x + 1, 2), div(y, 2)})
            condition = left in ["-", "L", "F", "S"] and right in ["-", "7", "J", "S"]

            case condition do
              true -> "-"
              false -> "."
            end

          true ->
            "."
        end
      end
    end
  end

  # Colapse the matrix, e.g. 22x22 to 10x10
  def colapse_matrix(matrix) do
    {size_x, size_y} = Tools.get_size(matrix)

    for y <- 0..(size_y - 2), rem(y + 1, 2) == 0 do
      for x <- 0..(size_x - 2), rem(x + 1, 2) == 0 do
        Tools.get_value(matrix, {x, y})
      end
    end
  end

  def loop(matrix, start), do: loop(matrix, start, start, start, "S", [])

  def loop(_matrix, start, _prev, curr, _pipe, points) when start == curr and length(points) != 0,
    do: points

  def loop(matrix, start, prev, curr, pipe, points) do
    curr
    |> pipes_to_points()
    |> Map.get(pipe)
    |> Enum.reduce_while(points, fn next, acc ->
      pipe = Tools.get_value(matrix, next)
      pipes = points_to_pipes(curr) |> Map.get(next)
      condition = pipe in pipes and prev != next

      case condition do
        true -> {:halt, loop(matrix, start, curr, next, pipe, [curr | acc])}
        false -> {:cont, acc}
      end
    end)
  end

  defp pipes_to_points({x, y}) do
    %{
      "|" => [{x, y - 1}, {x, y + 1}],
      "-" => [{x - 1, y}, {x + 1, y}],
      "L" => [{x, y - 1}, {x + 1, y}],
      "J" => [{x, y - 1}, {x - 1, y}],
      "7" => [{x - 1, y}, {x, y + 1}],
      "F" => [{x + 1, y}, {x, y + 1}],
      "S" => [{x, y - 1}, {x + 1, y}, {x, y + 1}, {x - 1, y}]
    }
  end

  defp points_to_pipes({x, y}) do
    %{
      {x, y - 1} => ["|", "7", "F", "S"],
      {x + 1, y} => ["-", "7", "J", "S"],
      {x, y + 1} => ["|", "L", "J", "S"],
      {x - 1, y} => ["-", "L", "F", "S"]
    }
  end

  defp out_of_loop(matrix, points, start) do
    {:ok, zeroes} = Agent.start_link(fn -> MapSet.new() end)

    out_of_loop(matrix, points, start, zeroes)
  end

  defp out_of_loop(matrix, points, {x, y} = curr, zeroes) do
    in_points? = MapSet.member?(points, curr)
    in_zeroes? = Agent.get(zeroes, fn set -> set end) |> MapSet.member?(curr)
    no_valid? = !Tools.valid?(matrix, curr)

    case in_points? or in_zeroes? or no_valid? do
      true ->
        zeroes

      false ->
        Agent.update(zeroes, fn set -> MapSet.put(set, curr) end)
        out_of_loop(matrix, points, {x, y - 1}, zeroes)
        out_of_loop(matrix, points, {x + 1, y}, zeroes)
        out_of_loop(matrix, points, {x, y + 1}, zeroes)
        out_of_loop(matrix, points, {x - 1, y}, zeroes)
    end
  end
end

Tests - Part 2

ExUnit.start(autorun: false)

defmodule PartTwoTest do
  use ExUnit.Case, async: true
  import PartTwo

  @input1 """
  ...........
  .S-------7.
  .|F-----7|.
  .||.....||.
  .||.....||.
  .|L-7.F-J|.
  .|..|.|..|.
  .L--J.L--J.
  ...........
  """
  @expected1 4

  @input2 """
  ..........
  .S------7.
  .|F----7|.
  .||....||.
  .||....||.
  .|L-7F-J|.
  .|II||II|.
  .L--JL--J.
  ..........
  """
  @expected2 4

  @input3 """
  .F----7F7F7F7F-7....
  .|F--7||||||||FJ....
  .||.FJ||||||||L7....
  FJL7L7LJLJ||LJ.L-7..
  L--J.L7...LJS7F-7L7.
  ....F-J..F7FJ|L7L7L7
  ....L7.F7||L7|.L7L7|
  .....|FJLJ|FJ|F7|.LJ
  ....FJL-7.||.||||...
  ....L---J.LJ.LJLJ...
  """
  @expected3 8

  @input4 """
  FF7FSF7F7F7F7F7F---7
  L|LJ||||||||||||F--J
  FL-7LJLJ||||||LJL-77
  F--JF--7||LJLJ7F7FJ-
  L---JF-JLJ.||-FJLJJ7
  |F|F-JF---7F7-L7L|7|
  |FFJF7L7F-JF7|JL---7
  7-L-JL7||F7|L7F-7F7|
  L.L7LFJ|||||FJL7||LJ
  L7JLJL-JLJLJL--JLJ.L
  """
  @expected4 10

  test "part two" do
    assert run(@input1) == @expected1
    assert run(@input2) == @expected2
    assert run(@input3) == @expected3
    assert run(@input4) == @expected4
  end
end

ExUnit.run()

Solution - Part 2

PartTwo.solve(puzzle_input)