Advent of Code 2025 - Day 9

We were all waiting for a (more) complex problem, and here it is, finally! As of now, it’s the only problem that has 3x more people who only solved the first part than those who solved both - for previous days, the ratio is ~6x the other way.

First part is fairly straightforward: just pick the “top” and “bottom” corners (to reduce the search somewhat), and calculate the maximum possible area between them (note the “mirroring” trick).

# Part 1
defmodule Y2025.Day09 do
  def corners(points, sort) do
    do_corners(points |> Enum.sort(sort), [], sort)
  end

  def do_corners([], acc, _), do: acc

  def do_corners([[x, y] = head | rest], acc, sort) do
    acc = [head | acc]

    points =
      rest
      |> Enum.reject(fn [x1, y1] ->
        if sort == :asc do
          x1 >= x && y1 >= y
        else
          x1 <= x && y1 <= y
        end
      end)

    do_corners(points, acc, sort)
  end

  def bottom_corners(points), do: corners(points, :asc)
  def top_corners(points), do: corners(points, :desc)

  def parse(s) do
    String.split(s, "\n")
    |> Enum.map(fn line -> String.split(line, ",") |> Enum.map(&String.to_integer/1) end)
  end

  def top_bottom_max(points) do
    for [x1, y1] <- bottom_corners(points), [x2, y2] <- top_corners(points) do
      max(x2 - x1 + 1, 0) * max(y2 - y1 + 1, 0)
    end
    |> Enum.max()
  end

  def part1(s) do
    points = parse(s)
    mirror_points = points |> Enum.map(fn [x, y] -> [-x, y] end)

    max(top_bottom_max(points), top_bottom_max(mirror_points))
  end
end

Part 2 is much less straightforward. I came up with the following algorithm - I’m sure both the algorithm and the implementation can be simplified (and I might do the latter tomorrow):

  1. We turn point coordinates into Point structures containing (a) the x, y coordinates themselves, (b) vectors v1 and v2 pointing to the next and previous points in the loop, (c) angle, which is either :sharp or :obtuse, depending on the “local” shape of the inside of the loop at this point, (d) a set of other points belonging strictly to top left, top right, bottom left, and bottom right quadrants (tp, tr, bt, br), and a set of sides of the loop that are at the top, bottom, left, or right from this point.

  2. We then select the maximum area between all the pairs p1, p2 of points that are allowed.

  3. A pair is allowed if: (a) the line from p1 to p2 lies inside both angles at p1 and p2, respectively; (b) there are no other points lying strictly within p1, p2 rectangle; (c) no other side intersects the p1, p2 line.

There are two less trivial pars of the implementation:

  1. We use a scalar product of two vectors to determine whether a given vector is inside the angle at point p, using the following fact: if v1, v2 are two orthogonal vectors, then v lies between them iff both scalar products v * v1 and v * v2 are positive. For obtuse angles, we reverse this check.

  2. To assign angles, we select the point with the minimum (lexicographic) coordinates - it is guaranteed to have a sharp angle - and then we follow the loop, keeping or flipping the angle, until we return to the starting point.

# Part 2
defmodule Y2025.Day09
  # ... part 1
  defmodule Point do
    defstruct [:left, :right, :top, :bottom, :v1, :v2, :x, :y, :angle, :tl, :tr, :bl, :br]

    def new([x, y], points) do
      tl = Enum.filter(points, fn [x1, y1] -> x1 < x && y1 > y end) |> MapSet.new()
      tr = Enum.filter(points, fn [x1, y1] -> x1 > x && y1 > y end) |> MapSet.new()
      bl = Enum.filter(points, fn [x1, y1] -> x1 < x && y1 < y end) |> MapSet.new()
      br = Enum.filter(points, fn [x1, y1] -> x1 > x && y1 < y end) |> MapSet.new()

      [_, y1] =
        Enum.filter(points, fn [x1, y1] -> x1 == x && y1 != y end)
        |> Enum.min_by(fn [_, y1] -> abs(y1 - y) end)

      v1 = [0, y1 - y]

      [x1, _] =
        Enum.filter(points, fn [x1, y1] -> x1 != x && y1 == y end)
        |> Enum.min_by(fn [x1, _] -> abs(x1 - x) end)

      v2 = [x1 - x, 0]

      %Point{v1: v1, v2: v2, x: x, y: y, tl: tl, tr: tr, br: br, bl: bl}
    end

    def assign_angles(points) do
      index = points |> Enum.into(%{}, fn p -> {[p.x, p.y], p} end)
      min_coords = index |> Map.keys() |> Enum.min()
      min = index[min_coords] |> Map.put(:angle, :sharp)
      index = index |> Map.put([min.x, min.y], min)
      do_assign_angles(index, min, 0, :v1, :sharp)
    end

    def do_assign_angles(index, _, n, _, _) when map_size(index) == n, do: index |> Map.values()

    def do_assign_angles(index, p, count, dir, kind) do
      p1 = index[add(p, if(dir == :v1, do: p.v1, else: p.v2))]

      new_kind =
        if (dir == :v1 && scalar(p.v2, p1.v2) >= 0) || (dir == :v2 && scalar(p.v1, p1.v1) >= 0) do
          kind
        else
          if kind == :sharp, do: :obtuse, else: :sharp
        end

      p1 = p1 |> Map.put(:angle, new_kind)
      index = index |> Map.put([p1.x, p1.y], p1)

      do_assign_angles(index, p1, count + 1, if(dir == :v1, do: :v2, else: :v1), new_kind)
    end

    def assign_sides(points) do
      sides = sides(points)

      points
      |> Enum.map(fn p ->
        top =
          Enum.filter(sides, fn {[_, y1], [_, y2]} -> y1 == y2 && y1 > p.y end) |> MapSet.new()

        bottom =
          Enum.filter(sides, fn {[_, y1], [_, y2]} -> y1 == y2 && y1 < p.y end) |> MapSet.new()

        left =
          Enum.filter(sides, fn {[x1, _], [x2, _]} -> x1 == x2 && x1 < p.x end) |> MapSet.new()

        right =
          Enum.filter(sides, fn {[x1, _], [x2, _]} -> x1 == x2 && x1 > p.x end) |> MapSet.new()

        %{p | left: left, right: right, top: top, bottom: bottom}
      end)
    end

    def sides(points) do
      points
      |> Enum.flat_map(fn p -> [{[p.x, p.y], add(p, p.v1)}, {[p.x, p.y], add(p, p.v2)}] end)
      |> Enum.map(fn
        {[x, y1], [x, y2]} -> {[x, min(y1, y2)], [x, max(y1, y2)]}
        {[x1, y], [x2, y]} -> {[min(x1, x2), y], [max(x1, x2), y]}
      end)
      |> Enum.uniq()
    end

    def add(%Point{x: x, y: y}, [x1, y1]), do: [x + x1, y + y1]

    def area(%Point{x: x1, y: y1}, %Point{x: x2, y: y2}),
      do: (abs(x1 - x2) + 1) * (abs(y1 - y2) + 1)

    def allowed?(%Point{} = p1, %Point{} = p2) do
      [p1, p2] = sort(p1, p2)
      v12 = [p2.x - p1.x, p2.y - p1.y]
      v21 = opposite(v12)

      inside_vector?(p1, v12) && inside_vector?(p2, v21) && !intersections?(p1, p2) &&
        no_vertexes_inside?(p1, p2)
    end

    def opposite([x, y]), do: [-x, -y]

    def inside_vector?(%Point{angle: angle} = p, v) do
      if angle == :sharp do
        between_vectors?(p, v)
      else
        !between_vectors?(p, v)
      end
    end

    def between_vectors?(%Point{v1: v1, v2: v2}, v) do
      scalar(v1, v) >= 0 && scalar(v2, v) >= 0
    end

    def scalar([x1, y1], [x2, y2]), do: x1 * x2 + y1 * y2

    def no_vertexes_inside?(p1, p2) do
      if p1.y < p2.y do
        MapSet.intersection(p1.tr, p2.bl) |> Enum.empty?()
      else
        MapSet.intersection(p1.br, p2.tl) |> Enum.empty?()
      end
    end

    def intersections?(p1, p2) do
      [p1, p2] = sort(p1, p2)

      {horizontal, vertical} =
        if p1.y < p2.y do
          {MapSet.intersection(p1.top, p2.bottom), MapSet.intersection(p1.right, p2.left)}
        else
          {MapSet.intersection(p1.bottom, p2.top), MapSet.intersection(p1.right, p2.left)}
        end

      Enum.concat(horizontal, vertical)
      |> Enum.any?(fn line -> intersects?(p1, p2, line) end)
    end

    def intersects?(%Point{x: x1, y: y1}, %Point{x: x2, y: y2}, {[x3, y], [x4, y]}) do
      x = (x2 - x1) / (y2 - y1) * (y - y1) + x1
      x3 < x && x < x4
    end

    def intersects?(%Point{x: x1, y: y1}, %Point{x: x2, y: y2}, {[x, y3], [x, y4]}) do
      y = (y2 - y1) / (x2 - x1) * (x - x1) + y1
      y3 < y && y < y4
    end

    def sort(p1, p2), do: Enum.sort_by([p1, p2], fn %Point{x: x, y: y} -> {x, y} end)
  end

  def part2(s) do
    points = parse(s)

    points =
      points
      |> Enum.map(fn p -> Point.new(p, points) end)
      |> Point.assign_angles()
      |> Point.assign_sides()

    pairs(points)
    |> Enum.map(fn {p1, p2} -> (Point.allowed?(p1, p2) && Point.area(p1, p2)) || 0 end)
    |> Enum.max()
  end

  def pairs(enum) do
    l = enum |> Enum.with_index()
    for {a, i} <- l, {b, j} <- l, i < j, do: {a, b}
  end
end