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

Advent of Code 2023

2023/day18.livemd

Advent of Code 2023

Mix.install([
  {:req, "~> 0.3.2"}
])

Day 18

input =
  "https://adventofcode.com/2023/day/18/input"
  |> Req.get!(headers: [cookie: "session=#{System.get_env("AOC_COOKIE")}"])
  |> Map.get(:body)
sample = """
R 6 (#70c710)
D 5 (#0dc571)
L 2 (#5713f0)
D 2 (#d2c081)
R 2 (#59c680)
D 2 (#411b91)
L 5 (#8ceee2)
U 2 (#caa173)
L 1 (#1b58a2)
U 2 (#caa171)
R 2 (#7807d2)
U 3 (#a77fa3)
L 2 (#015232)
U 2 (#7a21e3)
"""
defmodule A do
  def parse(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.map(&String.split(&1, " "))
    |> Enum.map(fn [dir, n, c] ->
      [_, n2, dir2] = Regex.run(~r/^\(#(.*)(.)\)$/, c)
      {n2, ""} = Integer.parse(n2, 16)

      dir2 =
        case dir2 do
          "0" -> "R"
          "1" -> "D"
          "2" -> "L"
          "3" -> "U"
        end

      {dir, String.to_integer(n), {dir2, n2}}
    end)
  end

  # We need the vertices in anti-clockwise order for the shoelace algorithm.
  # Building the list naturally in Elixir does that for us.
  defp construct_vertices(instructions) do
    instructions
    |> Enum.map(fn
      {"R", n} -> {n, 0}
      {"L", n} -> {-n, 0}
      {"D", n} -> {0, n}
      {"U", n} -> {0, -n}
    end)
    |> Enum.reduce({{0, 0}, []}, fn {dx, dy}, {{x, y}, vertices} ->
      p = {x + dx, y + dy}
      {p, [p | vertices]}
    end)
    |> elem(1)
  end

  defp area(vertices) do
    # we need this to wrap around
    next_vertices = tl(vertices) ++ [hd(vertices)]

    Enum.zip(vertices, next_vertices)
    |> Enum.map(fn {{x1, y1}, {x2, y2}} ->
      x1 * y2 - y1 * x2
    end)
    |> Enum.sum()
    |> abs()
    |> div(2)
  end

  # https://www.101computing.net/the-shoelace-algorithm/
  defp shoelace(instructions) do
    instructions
    |> construct_vertices()
    |> area()
  end

  defp perimeter(instructions) do
    instructions
    |> Enum.map(fn {_d, n} -> n end)
    |> Enum.sum()
  end

  # Pick's theorem  ->  A = i + b/2 - 1
  # where: A = area, i = interior points, b = perimeter
  # 
  # to get the number of points, rearrange to solve for i:
  # i = A - b/2 + 1
  # then add b (interior points + boundary points)
  # i + b = A + b/2 + 1
  defp picks_theorem(instructions) do
    area =
      instructions
      |> shoelace()

    perimeter =
      instructions
      |> perimeter()

    area + div(perimeter, 2) + 1
  end

  def part1(input) do
    input
    |> parse()
    |> then(&Enum.map(&1, fn {d, n, _c} -> {d, n} end))
    |> picks_theorem()
  end

  def part2(input) do
    input
    |> parse()
    |> then(&Enum.map(&1, fn {_d, _n, c} -> c end))
    |> picks_theorem()
  end
end

Part 1

input
|> A.part1()

Part 2

input
|> A.part2()