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
3 Likes

This one took a bit of thinking! This is more like I expected.

Part 1 I tried to do smartly by taking the outermost points, but failed - so I bruteforced it like yesterday. Generate all combinations of points, find the pair that create the biggest area. Not proud of that one, but okay.

Part 2 I calculated all of the edges created by the points in the input, by grouping them by shared coordinates. Then I took that bruteforced list from part 1, and found the first pair of points that created a rectangle with edges that didn’t intersect/exceed any edges in the list of all edges.

Honestly, I think trying to figure out if two lines intersected was the most annoying part of this :sweat_smile: And then the edge case of exceeding lines, eg. if these two edges were in the same row

---------- <- rectangle edge
------     <- input edge

It’s a little slower than I’d like (part 2 takes 1.5 seconds on my machine) but it’ll do for now!

1 Like

I think I have the same solution!
Full disclaimer I got some help because I was doing some ray-casting stuff and it’s not efficient :smiley:

defmodule AdventOfCode.Solutions.Y25.Day09 do
  alias AoC.Input

  def parse(input, _part) do
    input
    |> Input.stream!(trim: true)
    |> Enum.map(fn line ->
      [x, y] = String.split(line, ",", parts: 2)
      {String.to_integer(x), String.to_integer(y)}
    end)
  end

  def part_one(tiles) do
    Enum.reduce(all_rectangles_w_area(tiles), 0, fn {_, area}, best -> max(area, best) end)
  end

  defp area({xa, ya}, {xb, yb}) do
    (abs(xa - xb) + 1) * (abs(ya - yb) + 1)
  end

  def part_two(tiles) do
    {h_segments, v_segments} = build_segments([List.last(tiles) | tiles])

    tiles
    |> all_rectangles_w_area()
    |> Enum.filter(fn {rect, _} -> within_bounds?(rect, h_segments, v_segments) end)
    |> Enum.reduce(0, fn {_, area}, best -> max(area, best) end)
  end

  defp build_segments(tiles, horizontal \\ [], vertical \\ [])

  defp build_segments([{x, ya}, {x, yb} = b | rest], horizontal, vertical) do
    build_segments(
      [b | rest],
      horizontal,
      [{x, min(ya, yb), max(ya, yb)} | vertical]
    )
  end

  defp build_segments([{xa, y}, {xb, y} = b | rest], horizontal, vertical) do
    build_segments(
      [b | rest],
      [{y, min(xa, xb), max(xa, xb)} | horizontal],
      vertical
    )
  end

  defp build_segments([_], h, v) do
    {h, v}
  end

  defp all_rectangles_w_area([h | [_ | _] = t]) do
    Enum.map(t, &{rect(h, &1), area(h, &1)}) ++ all_rectangles_w_area(t)
  end

  defp all_rectangles_w_area([_]) do
    []
  end

  defp rect({x1, y1}, {x2, y2}) do
    {min(x1, x2), max(x1, x2), min(y1, y2), max(y1, y2)}
  end

  defp within_bounds?(rect, h_segments, v_segments) do
    not crosses_horizontal?(rect, h_segments) && not crosses_vertical?(rect, v_segments)
  end

  defp crosses_horizontal?(rect, h_segments) do
    {xa, xo, ya, yo} = rect

    Enum.any?(h_segments, fn h_segment ->
      {y, xha, xho} = h_segment
      y > ya and y < yo and not (xho <= xa or xha >= xo)
    end)
  end

  defp crosses_vertical?(rect, v_segments) do
    {xa, xo, ya, yo} = rect

    Enum.any?(v_segments, fn v_segment ->
      {x, yha, yho} = v_segment
      x > xa and x < xo and not (yho <= ya or yha >= yo)
    end)
  end
end

1 Like

Whew, finally got it! Part2 was hard for me.

Description:

  • I created a polygon around the input recognizing that tiles have a width and height of 1, so their corners are (0.5,0.5) off their coordinates depending on which way they’re turning.
  • Then I go through the box’s line segments and pick the largest that doesn’t cross that polygon anywhere.
  • Part 2 takes 380ms on my machine
defmodule RAoc.Solutions.Y25.Day09 do
  alias AoC.Input

  def parse(input, _part) do
    Input.read!(input)
    |> String.split("\n", trim: true)
    |> Enum.map(fn str ->
      [x, y] = String.split(str, ",")
      {String.to_integer(x), String.to_integer(y)}
    end)
  end

  def part_one(problem) do
    problem
    |> get_sorted_squares()
    |> List.first()
    |> elem(0)
  end

  def part_two(problem) do
    polygon = to_polygon(problem)

    problem
    |> get_sorted_squares()
    |> Enum.map(fn {area, {x1, y1}, {x2, y2}} ->
      # Turn these all into floats one time
      {area, {x1 + 0.0, y1 + 0.0}, {x2 + 0.0, y2 + 0.0}}
    end)
    |> Enum.find(fn box -> not square_outside_polygon?(box, polygon) end)
    |> elem(0)
  end

  def square_outside_polygon?({_area, {x1, y1}, {x2, y2}}, polygon) do
    line_seg_intersects_poly?({x1, y1}, {x2, y1}, polygon) or
      line_seg_intersects_poly?({x2, y1}, {x2, y2}, polygon) or
      line_seg_intersects_poly?({x2, y2}, {x1, y2}, polygon) or
      line_seg_intersects_poly?({x1, y2}, {x1, y1}, polygon)
  end

  def line_seg_intersects_poly?(p1, p2, [first | polygon]) do
    line_segment = {p1, p2}

    Enum.reduce_while(polygon, first, fn next, first ->
      case line_seg_intersects_line_seg?(line_segment, {first, next}) do
        true -> {:halt, true}
        false -> {:cont, next}
      end
    end)
    |> case do
      true -> true
      _ -> false
    end
  end

  def line_seg_intersects_line_seg?({{x1a, y1a}, {x2a, y2a}}, {{x1b, y1b}, {x2b, y2b}}) do
    # Take advantage of the fact that we know all these line segments are vertical or horizontal
    if x1a == x2a and y1b == y2b do
      # vertical and horizontal lines

      min(x1b, x2b) < x1a and x1a < max(x1b, x2b) and
        min(y1a, y2a) < y1b and y1b < max(y1a, y2a)
    else
      if x1b == x2b and y1a == y2a do
        # horizontal and vertical lines

        min(x1a, x2a) < x1b and x1b < max(x1a, x2a) and
          min(y1b, y2b) < y1a and y1a < max(y1b, y2b)
      else
        false
      end
    end
  end

  def to_polygon([first, second | rest]) do
    rest = rest ++ [first, second]

    polygon =
      Enum.reduce(rest, {first, second, []}, fn next, {prev, point, acc} ->
        fpoint = get_corner_point(prev, point, next)
        {point, next, [fpoint | acc]}
      end)
      |> elem(2)
      |> Enum.reverse()

    polygon ++ [List.first(polygon)]
  end

  def get_corner_point({x_prev, y_prev}, {x, y}, {x_next, y_next}) do
    l1_dir = get_dir({x_prev, y_prev}, {x, y})
    l2_dir = get_dir({x, y}, {x_next, y_next})

    case {l1_dir, l2_dir} do
      {:right, :down} ->
        {x + 0.5, y - 0.5}

      {:right, :up} ->
        {x - 0.5, y - 0.5}

      {:left, :down} ->
        {x + 0.5, y + 0.5}

      {:left, :up} ->
        {x - 0.5, y + 0.5}

      {:down, :right} ->
        {x + 0.5, y - 0.5}

      {:down, :left} ->
        {x + 0.5, y + 0.5}

      {:up, :right} ->
        {x - 0.5, y - 0.5}

      {:up, :left} ->
        {x - 0.5, y + 0.5}
    end
  end

  def get_dir({x1, y1}, {x2, y2}) when x1 == x2 and y1 < y2, do: :down
  def get_dir({x1, y1}, {x2, y2}) when x1 == x2 and y1 > y2, do: :up
  def get_dir({x1, y1}, {x2, y2}) when x1 < x2 and y1 == y2, do: :right
  def get_dir({x1, y1}, {x2, y2}) when x1 > x2 and y1 == y2, do: :left

  def get_sorted_squares(coords) do
    coords = MapSet.new(coords)

    Enum.reduce(coords, {coords, []}, fn coord1, {others, distances} ->
      others = MapSet.delete(others, coord1)

      {others,
       Enum.map(others, fn coord2 ->
         {area(coord1, coord2), coord1, coord2}
       end) ++ distances}
    end)
    |> elem(1)
    |> Enum.sort(:desc)
  end

  def area({x1, y1}, {x2, y2}) do
    (abs(x1 - x2) + 1) * (abs(y1 - y2) + 1)
  end
end

For fun I tried a version of the above that works only in integers, by multiplying the coordinates by 2 and then dividing the area by 4 at the end. That brings the time down from 380ms to 250ms.

2 Likes

Not the fastest for part 2 (1-2s), but works:

Setup

tiles =
  puzzle_input
  |> String.split()
  |> Enum.map(fn raw ->
    raw
    |> String.split(",")
    |> Enum.map(&String.to_integer/1)
    |> List.to_tuple()
  end)

Impl

defmodule Combinatorics do
  def combinations2(list) do
    Stream.unfold(list, fn
      [] -> nil
      [x | rest] ->
        curr = for y <- rest, do: [x, y]

        {curr, rest}
    end)
    |> Stream.flat_map(& &1)
  end
end

defmodule Rect do
  require Record

  Record.defrecordp(:rect, l: 0, t: 0, r: 0, b: 0)

  def new({ax, ay}, {bx, by}) do
    rect(l: min(ax, bx), r: max(ax, bx), t: min(ay, by), b: max(ay, by))
  end

  def area(rect() = r) do
    width(r) * height(r)
  end

  def intersect?(
        rect(l: al, r: ar, t: at, b: ab),
        rect(l: bl, r: br, t: bt, b: bb)
      ) do
    al < br and ar > bl and at < bb and ab > bt
  end

  def width(rect(r: r, l: l)), do: r - l + 1
  def height(rect(t: t, b: b)), do: b - t + 1

  def to_svg(rect(l: x, t: y) = r, opts \\ []) do
    ~s"""
    <rect x="#{x}" y="#{y}" width=#{width(r)} height="#{height(r)}"
    #{Enum.map_join(opts, " ", fn {k, v} -> ~s(#{k}="#{v}") end)} />
    """
  end
end

rects =
  Combinatorics.combinations2(tiles)
  |> Stream.map(fn [a, b] -> Rect.new(a, b) end)
  |> Enum.sort()

Part 1

rects
|> Enum.max_by(&Rect.area/1)
|> Rect.area()

Part 2

edges =
  tiles
  |> Enum.chunk_every(2, 1, tiles)
  |> Enum.map(&apply(Rect, :new, &1))
  |> Enum.sort()

rects
|> Enum.reduce({0, nil}, fn r, {max, p} ->
  a = Rect.area(r)

  if a > max and not Enum.any?(edges, &Rect.intersect?(r, &1)) do
    {a, r}
  else
    {max, p}
  end
end)
2 Likes

Sheesh, I’ve been bending over backwards in my code to do this. Thanks!

I’m not sure how that works. Since you call the function with a being the rectangle and b being the edge, I interpret this as “check if the rectangle is fully included in the edge”, which will never be true since edges have either a width or a height of 1.

What am I missing here?

Ah nevermind I get it. I should note that somewhere, I always have a hard time writing all the cases for the intersections but yours is very clean.

(PS: I also used svg rendering to see more clearly what I was dealing with, definitely useful!)

1 Like
defmodule Aoc25.Day09 do
  @moduledoc "https://adventofcode.com/2025/day/9"

  require Aoc

  @doc ~S"""
  ## Examples
    iex> Aoc25.Day09.part1("example.txt")
    50
    iex> Aoc25.Day09.part1("input.txt")
    4746238001
  """
  def part1(file_path) do
    {_points, area} =
      file_path
      |> Aoc.get_input()
      |> Aoc.extract_numbers()
      |> Enum.chunk_every(2)
      |> Enum.map(&List.to_tuple/1)
      |> areas()
      |> Enum.max_by(&elem(&1, 1))

    area
  end

  defp areas(list, acc \\ []) do
    case list do
      [point | rest_points] ->
        acc =
          acc ++
            Enum.map(rest_points, fn other_point ->
              {{point, other_point}, rectangle(point, other_point)}
            end)

        areas(rest_points, acc)

      [] ->
        Enum.sort_by(acc, fn {_points, distance} -> distance end, :desc)
    end
  end

  defp rectangle({x1, y1}, {x2, y2}) do
    (abs(x1 - x2) + 1) * (abs(y1 - y2) + 1)
  end

  @doc ~S"""
  ## Examples
    iex> Aoc25.Day09.part2("example.txt")
    24
    iex> Aoc25.Day09.part2("input.txt")
    1552139370
    iex> Aoc25.Day09.part2("example_edgecase.txt")
    12
  """
  def part2(file_path) do
    vs =
      file_path
      |> Aoc.get_input()
      |> Aoc.extract_numbers()
      |> Enum.map(&Kernel./(&1, 1))
      |> Enum.chunk_every(2)
      |> Enum.map(&List.to_tuple/1)

    edges = Enum.zip(vs, tl(vs) ++ [hd(vs)])

    areas = areas(vs)

    # this works for this example. but it's not a general solution
    # this will fail if there is a big c shaped where the void is bigger.
    # it will identify the void outside as the bigger square
    Enum.find_value(areas, fn {{{x1, y1}, {x2, y2}}, area} ->
      # We change order here to properly provide line_segments
      p1 = {x1, y1}
      p2 = {x1, y2}
      p3 = {x2, y2}
      p4 = {x2, y1}

      no_intersections? =
        edges
        |> Enum.flat_map(
          &[
            intersect?({p1, p2}, &1),
            intersect?({p2, p3}, &1),
            intersect?({p3, p4}, &1),
            intersect?({p4, p1}, &1),
            intersect?({p1, p3}, &1),
            intersect?({p2, p4}, &1)
          ]
        )
        |> Enum.all?(&(&1 == false))

      if no_intersections? do
        trunc(area)
      end
    end)
  end

  defp intersect?(line_seg1, line_seg2) do
    case Aoc.intersect_at_point(line_seg1, line_seg2) do
      {:cross, _} -> true
      _ -> false
    end
  end

  @doc ~S"""
  ## Examples
    iex> Aoc25.Day09.part2_generic("example.txt")
    24
    iex> Aoc25.Day09.part2_generic("input.txt")
    1552139370
    iex> Aoc25.Day09.part2_generic("example_edgecase.txt")
    12
  """
  def part2_generic(file_path) do
    vs =
      file_path
      |> Aoc.get_input()
      |> Aoc.extract_numbers()
      |> Enum.chunk_every(2)
      |> Enum.map(&List.to_tuple/1)

    edges = Enum.zip(vs, tl(vs) ++ [hd(vs)])

    areas = areas(vs)

    fun = fn point, {_, acc} ->
      {in?, acc} = wind(edges, point, acc)

      if in? do
        {:cont, {in?, acc}}
      else
        {:halt, {in?, acc}}
      end
    end

    Enum.reduce_while(areas, %{}, fn {{{x1, y1}, {x2, y2}}, total_area}, acc ->
      p1 = {x1, y1}
      p2 = {x1, y2}
      p3 = {x2, y2}
      p4 = {x2, y1}

      # this nonsense speed things up by quite a lot, we don't need to create all borders at once
      with {true, acc} <- wind(edges, p1, acc),
           {true, acc} <- wind(edges, p2, acc),
           {true, acc} <- wind(edges, p3, acc),
           {true, acc} <- wind(edges, p4, acc),
           {true, acc} <- Enum.reduce_while(fill(p1, p2), {nil, acc}, fun),
           {true, acc} <- Enum.reduce_while(fill(p2, p3), {nil, acc}, fun),
           {true, acc} <- Enum.reduce_while(fill(p3, p4), {nil, acc}, fun),
           {true, _acc} <- Enum.reduce_while(fill(p4, p1), {nil, acc}, fun) do
        {:halt, total_area}
      else
        {false, acc} -> {:cont, acc}
      end
    end)
  end

  defp fill({x1, y1}, {x2, y2}) do
    if x1 == x2 do
      Enum.map(y1..y2, &{x1, &1})
    else
      Enum.map(x1..x2, &{&1, y1})
    end
  end

  defp wind(edges, {px, py}, cache) do
    case Map.get(cache, {px, py}) do
      nil ->
        value = do_wind(edges, {px, py})
        {value, Map.put(cache, {px, py}, value)}

      value ->
        {value, cache}
    end
  end

  @inside 1

  defp do_wind(edges, p3) do
    edges
    |> Enum.reduce_while(0, fn {p1, p2}, acc ->
      cond do
        in_border?(p1, p2, p3) -> {:halt, @inside}
        upwards?(p1, p2, p3) and x_intersect?(p1, p2, p3) -> {:cont, acc + 1}
        downwards?(p1, p2, p3) and x_intersect?(p1, p2, p3) -> {:cont, acc - 1}
        :else -> {:cont, acc}
      end
    end)
    |> then(&(abs(&1) > 0))
  end

  defp in_border?({x1, y1}, {x2, y2}, {px, py}) do
    cross_product = (y2 - y1) * (px - x1) - (x2 - x1) * (py - y1)

    cross_product == 0 and min(x1, x2) <= px and px <= max(x1, x2) and min(y1, y2) <= py and py <= max(y1, y2)
  end

  defp upwards?({_x1, y1}, {_x2, y2}, {_px, py}) do
    y1 <= py and py < y2
  end

  defp downwards?({_x1, y1}, {_x2, y2}, {_px, py}) do
    y2 <= py and py < y1
  end

  defp x_intersect?({x1, y1}, {x2, y2}, {px, py}) do
    px < x1 + (py - y1) * (x2 - x1) / (y2 - y1)
  end
end

Cool problem, it has quite a few solutions, maybe some inputs were not so lucky.

I noticed my first solution for part2 is not complete, because c-shaped polygons would be show as inside of the polygon.

I’ve also did a slower (with some wierd optimizations) to check all cases.

2 Likes

optimized my solution by using my combinations helper instead of permutations - much faster now :slight_smile: Want to keep every solution under a second if I can…

Name                     ips        average  deviation         median         99th %
day 09, part 1         72.77       13.74 ms     ±8.83%       13.58 ms       16.81 ms
day 09, part 2          3.20      312.69 ms     ±1.75%      311.01 ms      328.37 ms
1 Like

Part1 was almost the same as Day8.

Part 2, I tried a few solutions, but wasn’t able to reach an efficient one. My first defeat this year.
I may come back to it after reading the solutions above, but… I’m probably going to learn Erlang over the holidays instead :smiley:

1 Like

Per usual, mine is golf’d to oblivion:

defmodule Day09 do
  def part1(file), do: file |> parse() |> rects() |> Enum.map(&area/1) |> Enum.max()

  def part2(file) do
    corners = parse(file)
    corners |> rects() |> Enum.sort_by(&area/1, :desc) |> find(edges(corners)) |> area()
  end

  def parse(file), do: file |> File.read!() |> String.split("\n", trim: true) |> Enum.map(&line/1)
  def line(line), do: line |> String.split(",", trim: true) |> Enum.map(&String.to_integer/1)
  def rects(corners), do: for(a <- corners, b <- corners, a != b, do: rect([a, b]))
  def rect([[ax, ay], [bx, by]]), do: {[min(ax, bx), min(ay, by)], [max(ax, bx), max(ay, by)]}
  def edges([h | t]), do: Enum.chunk_every([h | t] ++ [h], 2, 1, :discard) |> Enum.map(&rect/1)
  def area({[x1, y1], [x2, y2]}), do: (abs(x1 - x2) + 1) * (abs(y1 - y2) + 1)
  def intersect?({[a, b], [c, d]}, {[e, f], [g, h]}), do: a < g and c > e and b < h and d > f
  def find(rs, edges), do: Enum.find(rs, fn r -> Enum.all?(edges, &(not intersect?(r, &1))) end)
end

The only thing of note is that I on part 2, I checked for intersection by pretending that edges were degenerate rectangles which greatly simplified the logic.

I finally got some time to finish my solution for day 9.

Here is my cleaned up solution stripped of all debugging code. A slightly interesting thing about my solution is that I do ray-casting or walking from one corner to another, but only in the horizontal directions. To check the vertical lines, I ran the same check on the tiles rotated 90 degrees.

The combined runtime for both parts and the examples are 0.4 seconds on my computer.

defmodule Day09 do
  def part1(input) do
    tiles = parse(input)
    tiles
    |> Enum.flat_map(fn a ->
      Enum.flat_map(tiles, fn b ->
        if a < b, do: [area(a, b)], else: []
      end)
    end)
    |> Enum.sort(:desc)
    |> Enum.take(1)
    |> hd
  end

  def part2(input) do
    tiles = parse(input)

    transposed = tiles
    |> Enum.map(fn {x, y} -> {y, x} end)
    |> Enum.reverse

    plain = prepare_tiles(tiles)
    transposed = prepare_tiles(transposed)

    {tiles, _, _} = plain

    tiles = Enum.sort(tiles)
    Enum.reduce(tiles, 0, fn a, max_area ->
      Enum.reduce(tiles, max_area, fn b, max_area ->
        if a >= b do
          max_area
        else
          area = area(a, b)
          if area <= max_area do
            max_area
          else
            region1 = [a, b]
            region2 = Enum.map(region1, fn {x, y} -> {y, x} end)
            if solve_one(region1, plain) and solve_one(region2, transposed) do
              area
            else
              max_area
            end
          end
        end
      end)
    end)
  end

  defp prepare_tiles(tiles) do
    edges = Enum.chunk_every(tiles, 2, 1, tiles)
    |> Enum.map(&List.to_tuple/1)

    convex = Enum.chunk_every(edges, 2, 1, edges)
    |> Enum.map(fn [{p1, p2}, {p2, p3}] ->
      convex? = if cross_z(p1, p2, p3) > 0, do: :convex, else: :concave
      {p2, {convex?, p1, p3}}
    end)
    |> Map.new

    {tiles, edges, convex}
  end

  # Given two diagonal corners, check that the two horizontal
  # lines going to the other two corners of the rectangle
  # only have red and green tiles.
  defp solve_one([a, b], {_tiles, edges, convex}) do
    {lx, y1} = a
    {rx, y2} = b
    c = {lx, y2}
    d = {rx, y1}

    cond do
      c === a or d === b ->
        [{b, c, sign(lx - rx)}]
      true ->
        [{b, c, sign(lx - rx)}, {a, d, sign(rx - lx)}]
    end
    |> Enum.all?(fn {from, to, dir} ->
      if from === to do
        false
      else
        walk(from, to, dir, edges, convex)
      end
    end)
  end

  defp sign(int) do
    cond do
      int < 0 -> -1
      int > 0 -> 1
      true -> 0
    end
  end

  defp area({x1, y1}, {x2, y2}) do
    (abs(x1 - x2) + 1) * (abs(y1 - y2) + 1)
  end

  defp walk(from, to, dir, edges, convex) do
    case classify_corner(from, dir, convex) do
      :outside ->
        false
      where ->
        {from_x, from_y} = from
        {to_x, _} = to

        # Only keep the relevant vertical edges.
        edges = edges
        |> Enum.filter(fn {{_,y1}, {_,y2}} ->
          y1 !== y2 and from_y in min(y1, y2)..max(y1, y2)
        end)

        case dir do
          -1 ->
            edges
            |> Enum.reject(fn {{x1,_}, {x1,_}} ->
              from_x < x1 or to_x > x1
            end)
            |> Enum.sort(:desc)
            |> then(fn edges ->
              walk_1(tl(edges), dir, to, convex, where)
            end)
          1 ->
            edges
            |> Enum.reject(fn {{x1,_}, {x1,_}} ->
              x1 < from_x or x1 > to_x
            end)
            |> Enum.sort
            |> then(fn edges ->
              walk_1(tl(edges), dir, to, convex, where)
            end)
        end
    end
  end

  # Classify a corner with respect to the horizontal direction.
  # Returns one of :edge, :inside, or :outside.
  defp classify_corner(from, dir, convex) do
    {from_x, _} = from
    other_corner = case convex do
                     %{^from => {_, {^from_x, _}, other}} -> other
                     %{^from => {_, other, {^from_x, _}}} -> other
                   end

    {diff_x, _} = sub(other_corner, from)
    case sign(diff_x) do
      ^dir ->
        # Walking in the given direction is along the edge to
        # the other corner.
        :edge
      _ ->
        # We will not walk along the edge, but we will enter move to
        # to the inside or outside.
        case is_convex(convex, from) do
          :concave -> :inside
          :convex -> :outside
        end
    end
  end

  # Walk from a corner point horizontally in either direction.
  # Returns `true` if the destination point `to` can be reached
  # by only passing red and green tiles.
  defp walk_1(_, _dir, _to, _convex, :outside) do
    false
  end
  defp walk_1([], _dir, _to, _convex, _) do
    true
  end
  defp walk_1([{a, b} | edges], dir, to, convex, where) do
    {to_x, to_y} = to
    {ab_x, a_y} = a
    {_, b_y} = b
    cond do
      dir === -1 and ab_x <= to_x ->
        # Destination is on the edge or at the corner.
        where === :edge or where === :inside
      dir === 1 and to_x <= ab_x ->
        # Destination is on the edge or at the corner.
        where === :edge or where === :inside
      a_y === to_y and corner?(convex, a) ->
        # Destination is beyond this corner.
        where = pass_corner(where, is_convex(convex, a))
        walk_1(edges, dir, to, convex, where)
      b_y === to_y and corner?(convex, b) ->
        # Destination is beyond this corner.
        where = pass_corner(where, is_convex(convex, b))
        walk_1(edges, dir, to, convex, where)
      true ->
        # Destination is beyond this edge.
        false
    end
  end

  defp corner?(convex, point) do
    Map.has_key?(convex, point)
  end

  defp is_convex(convex, corner) do
    {convex?, _, _} = Map.get(convex, corner)
    convex?
  end

  defp pass_corner(:edge, :convex), do: :outside
  defp pass_corner(:edge, :concave), do: :inside
  defp pass_corner(:inside, :concave), do: :edge

  defp cross_z({x1, y1}, {x2, y2}, {x3, y3}) do
    (x2 - x1) * (y3 - y2) - (y2 - y1) * (x3 - x2)
  end

  defp sub({x1, y1}, {x2, y2}) do
    {x1 - x2, y1 - y2}
  end

  defp parse(input) do
    input
    |> Enum.map(fn line ->
      line
      |> String.split(",")
      |> Enum.map(&String.to_integer/1)
      |> List.to_tuple
    end)
  end
end

1 Like