vkryukov
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):
-
We turn point coordinates into
Pointstructures containing (a) thex,ycoordinates themselves, (b) vectorsv1andv2pointing to the next and previous points in the loop, (c)angle, which is either:sharpor: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 thetop,bottom,left, orrightfrom this point. -
We then select the maximum area between all the pairs
p1,p2of points that are allowed. -
A pair is allowed if: (a) the line from
p1top2lies inside both angles atp1andp2, respectively; (b) there are no other points lying strictly withinp1, p2rectangle; (c) no other side intersects thep1, p2line.
There are two less trivial pars of the implementation:
-
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: ifv1,v2are two orthogonal vectors, thenvlies between them iff both scalar productsv * v1andv * v2are positive. For obtuse angles, we reverse this check. -
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
Trending in Challenges
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #performance
- #security










First 10 of 11 Posts!
sevenseacat
This one took a bit of thinking! This is more like I expected.
https://github.com/sevenseacat/advent_of_code/blob/main/lib/y2025/day09.ex
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
And then the edge case of exceeding lines, eg. if these two edges were in the same row
It’s a little slower than I’d like (part 2 takes 1.5 seconds on my machine) but it’ll do for now!
lud
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
rvnash
Whew, finally got it! Part2 was hard for me.
Description:
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.
hauleth
Not the fastest for part 2 (1-2s), but works:
Setup
Impl
Part 1
Part 2
rvnash
Sheesh, I’ve been bending over backwards in my code to do this. Thanks!
lud
I’m not sure how that works. Since you call the function with
abeing the rectangle andbbeing 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!)
lpasz
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.
sevenseacat
optimized my solution by using my
Want to keep every solution under a second if I can…
combinationshelper instead ofpermutations- much faster nowadamu
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
billylanchantin
Per usual, mine is golf’d to oblivion:
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.
Last Post!
bjorng
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.