AoC 2025: Day 9

Main thread for Day 9 of the 2025 advent of code. Feel free to discuss the challenge and ask questions. If particular discussions become large, we can break them off into new threads.

Notice that Advent of Code has changed it’s schedule and there will only be 12 days of puzzles this year.

Some Rules:

  1. Please try to keep spoilers within spoiler tags. This is done by typing [spoiler]text to be blurred[\spoiler]. If you have longer sections you can also use the [details=“Title”] tags. If you use the new WYSIWYG editor, remember to close the details section, as it is opened by default. This is an issue (feature?) of the newest discourse updates. If you use markdown, then you can remove the “open" parameter (not in by default unless you use the WYSIWYG editor)
  2. Have Fun

Day 9 Challenge

Templates:

Resources:

1 Like

Not the most efficient solution but I survived for another day.

edit: I figured out a performance trick by mapping the x/y points to an array index

1 Like

Harder today.

Record the in-edge and out-edge directions of all points. For the point where x is the lowest and y is relatively the lowest, we can ensure that it must be a convex Angle. Therefore, based on its in-edge and out-edge, we can determine whether the drawing process of all edges is clockwise or counterclockwise.

Two checks were conducted successively.
For the two points of the defined rectangle, the directions of their incoming and outgoing edges were checked. It was determined whether the corner was convex or concave based on whether the drawing was clockwise, and it was judge whether it was valid based on the position of the defined points in the rectangle (topleft/bottomleft/topright/bottomright).

If the inspection is passed, conduct an edge cutting inspection, which is relatively simple.

The code is a bit messy, especially not good at naming so many concepts.

pub fn WrapId(comptime cap: usize) type {
    return struct {
        pub fn next(id: usize) usize {
            return (id + 1) % cap;
        }
        pub fn prev(id: usize) usize {
            return (id + cap - 1) % cap;
        }
    };
}
pub fn part2(input: []const u8) !u64 {
    const point_num = 496;
    const Direction = enum(u4) {
        const BackingVec = packed struct(u4) {
            x: i2,
            y: i2,
            fn cross(in: @This(), out: @This()) i2 {
                return in.x * out.y - in.y * out.x;
            }
            fn turnIsConvex(in: @This(), out: @This(), is_clockwise: bool) bool {
                return (in.cross(out) > 0) == is_clockwise;
            }
        };
        left = @bitCast(@as(BackingVec, .{ .x = -1, .y = 0 })),
        up = @bitCast(@as(BackingVec, .{ .x = 0, .y = -1 })),
        right = @bitCast(@as(BackingVec, .{ .x = 1, .y = 0 })),
        down = @bitCast(@as(BackingVec, .{ .x = 0, .y = 1 })),
        fn vec(self: @This()) BackingVec {
            return @bitCast(@intFromEnum(self));
        }
    };
    const Point = packed struct(u64) {
        y: u32,
        x: u32,
        fn lessThan(_: void, a: @This(), b: @This()) bool {
            return @as(u64, @bitCast(a)) < @as(u64, @bitCast(b));
        }
        fn direction(from: @This(), to: @This()) Direction {
            const dx = @as(i33, to.x) - @as(i33, from.x);
            const dy = @as(i33, to.y) - @as(i33, from.y);
            const backing: Direction.BackingVec = .{
                .x = @intCast(std.math.sign(dx)),
                .y = @intCast(std.math.sign(dy)),
            };
            return @enumFromInt(@as(u4, @bitCast(backing)));
        }
    };
    var edges: [point_num]struct {
        from_point: Point,
        direction: Direction,
    } = undefined;
    const iw = WrapId(point_num);
    var line_it = std.mem.splitScalar(u8, input, '\n');
    const first_line = line_it.next().?;
    var first_line_it = std.mem.splitScalar(u8, first_line, ',');
    edges[0].from_point = .{
        .x = try std.fmt.parseUnsigned(u32, first_line_it.next().?, 10),
        .y = try std.fmt.parseUnsigned(u32, first_line_it.next().?, 10),
    };
    var min_point_id: usize = 0;
    for (1..point_num) |id| {
        const line = line_it.next().?;
        var it = std.mem.splitScalar(u8, line, ',');
        const point: Point = .{
            .x = try std.fmt.parseUnsigned(u32, it.next().?, 10),
            .y = try std.fmt.parseUnsigned(u32, it.next().?, 10),
        };
        edges[id].from_point = point;
        if (Point.lessThan({}, point, edges[min_point_id].from_point)) min_point_id = id;
        const last_point = edges[iw.prev(id)].from_point;
        edges[iw.prev(id)].direction = last_point.direction(point);
    }
    edges[point_num - 1].direction = edges[point_num - 1].from_point.direction(edges[0].from_point);
    const is_clockwise = blk: {
        const min_point_vout = edges[min_point_id].direction.vec();
        const min_point_vin = edges[iw.prev(min_point_id)].direction.vec();
        break :blk min_point_vin.cross(min_point_vout) > 0;
    };
    var largest_area: u64 = 0;
    const Rect = struct {
        min_x: u32,
        max_x: u32,
        min_y: u32,
        max_y: u32,
        const Corner = enum(u4) {
            const BackingVec = packed struct(u4) {
                x: i2,
                y: i2,
            };
            topleft = @bitCast(@as(BackingVec, .{ .x = 1, .y = 1 })),
            topright = @bitCast(@as(BackingVec, .{ .x = -1, .y = 1 })),
            bottomleft = @bitCast(@as(BackingVec, .{ .x = 1, .y = -1 })),
            bottomright = @bitCast(@as(BackingVec, .{ .x = -1, .y = -1 })),
            fn growVec(self: @This()) BackingVec {
                return @bitCast(@intFromEnum(self));
            }
            fn fromBacking(vec: BackingVec) @This() {
                return @enumFromInt(@as(u4, @bitCast(vec)));
            }
            fn opposite(self: @This()) @This() {
                return switch (self) {
                    .topleft => .bottomright,
                    .topright => .bottomleft,
                    .bottomleft => .topright,
                    .bottomright => .topleft,
                };
            }
            fn validate(
                self: @This(),
                in_dir: Direction,
                out_dir: Direction,
                is_cw: bool,
            ) bool {
                const is_convex = (in_dir.vec().cross(out_dir.vec()) > 0) == is_cw;
                const growth = self.growVec();
                if (is_convex) {
                    return (growth.x == out_dir.vec().x or growth.x == -in_dir.vec().x) and
                        (growth.y == out_dir.vec().y or growth.y == -in_dir.vec().y);
                } else {
                    const is_gap_x = (growth.x == out_dir.vec().x or growth.x == -in_dir.vec().x);
                    const is_gap_y = (growth.y == out_dir.vec().y or growth.y == -in_dir.vec().y);
                    return !(is_gap_x and is_gap_y);
                }
            }
        };
        fn fromDiagonalWithP1Corner(p1: Point, p2: Point) struct { @This(), ?Corner } {
            const p1_is_left = p1.x < p2.x;
            const p1_is_top = p1.y < p2.y;
            return .{
                .{
                    .min_x = if (p1_is_left) p1.x else p2.x,
                    .max_x = if (p1_is_left) p2.x else p1.x,
                    .min_y = if (p1_is_top) p1.y else p2.y,
                    .max_y = if (p1_is_top) p2.y else p1.y,
                },
                if (p1.x == p2.x or p1.y == p2.y) null else .fromBacking(.{
                    .x = if (p1_is_left) 1 else -1,
                    .y = if (p1_is_top) 1 else -1,
                }),
            };
        }
        fn area(self: @This()) u64 {
            return @as(u64, self.max_x - self.min_x + 1) * @as(u64, self.max_y - self.min_y + 1);
        }
        fn isEdgeCut(self: @This(), p1: Point, p2: Point) bool {
            if (p1.x == p2.x) {
                const edge_x = p1.x;
                if (edge_x > self.min_x and edge_x < self.max_x) {
                    const seg_min_y = @min(p1.y, p2.y);
                    const seg_max_y = @max(p1.y, p2.y);
                    if (seg_min_y < self.max_y and seg_max_y > self.min_y) {
                        return true;
                    }
                }
            } else if (p1.y == p2.y) {
                const edge_y = p1.y;
                if (edge_y > self.min_y and edge_y < self.max_y) {
                    const seg_min_x = @min(p1.x, p2.x);
                    const seg_max_x = @max(p1.x, p2.x);
                    if (seg_min_x < self.max_x and seg_max_x > self.min_x) {
                        return true;
                    }
                }
            } else unreachable;
            return false;
        }
    };
    for (0..point_num) |i| {
        scan_rect: for (i + 1..point_num) |j| {
            const p1 = edges[i].from_point;
            const p2 = edges[j].from_point;
            const rect: Rect, const maybe_p1corner: ?Rect.Corner = Rect.fromDiagonalWithP1Corner(p1, p2);
            const area = rect.area();
            if (area <= largest_area) continue :scan_rect;
            if (maybe_p1corner) |p1corner| {
                const p2corner = p1corner.opposite();
                if (!p1corner.validate(edges[iw.prev(i)].direction, edges[i].direction, is_clockwise)) continue :scan_rect;
                if (!p2corner.validate(edges[iw.prev(j)].direction, edges[j].direction, is_clockwise)) continue :scan_rect;
            }
            for (0..point_num) |cut_id| {
                const cutter1 = edges[cut_id].from_point;
                const cutter2 = edges[iw.next(cut_id)].from_point;
                if (rect.isEdgeCut(cutter1, cutter2)) continue :scan_rect;
            }
            largest_area = area;
        }
    }
    return largest_area;
}

1 Like

Day 9 took me bit, especially with part 2. After I got my code working for the example input it took me some back and forth to figure out why it’s not working for the puzzle input. The final enlightenment appeared to me when I hacked together a quick and dirty way of visualizing my puzzle input for part 2 as svg.

Part 1:

fn calcArea(x0: u64, x1: u64, y0: u64, y1: u64) u64 {
    const vec_a: u64 = @abs(@as(i64, @intCast(x0)) - @as(i64, @intCast(x1))) + 1;
    const vec_b: u64 = @abs(@as(i64, @intCast(y0)) - @as(i64, @intCast(y1))) + 1;
    return vec_a * vec_b;
}

fn distance(a: @Vector(2, u64), b: @Vector(2, u64)) f64 {
    const vec: @Vector(2, f64) = @as(@Vector(2, f64), @floatFromInt(a)) - @as(@Vector(2, f64), @floatFromInt(b));
    return @sqrt(@exp2(vec))[0];
}

fn findLeftMostLowest(list: *std.array_list.Managed(@Vector(2, u64))) @Vector(2, u64) {
    std.debug.assert(list.items.len > 2);
    var result = list.items[0];
    for (list.items) |p| {
        if (p[0] < result[0]) {
            result = p;
            if (p[1] < result[1]) {
                result = p;
            }
        }
    }
    return result;
}

/// Cast a ray from point p to the right
/// Count how many times it intersects the path
/// count is odd => inside, count is even => outside
fn insidePath(path: *std.array_list.Managed(@Vector(2, u64)), p: @Vector(2, u64)) bool {
    var inside = false;
    for (0..path.items.len) |i| {
        const a = path.items[i];
        const b = path.items[(i + 1) % path.items.len];

        // Convert to signed integers for comparison
        const ax = @as(i64, @intCast(a[0]));
        const ay = @as(i64, @intCast(a[1]));
        const bx = @as(i64, @intCast(b[0]));
        const by = @as(i64, @intCast(b[1]));
        const px = @as(i64, @intCast(p[0]));
        const py = @as(i64, @intCast(p[1]));

        // Check if edge crosses the horizontal ray from point p to the right
        // Edge must straddle the horizontal line at py
        if ((ay >= py) != (by >= py)) {
            // Calculate x-coordinate of intersection point
            // Using: x = ax + (py - ay) * (bx - ax) / (by - ay)
            const slope_num = (py - ay) * (bx - ax);
            const slope_den = by - ay;
            const intersect_x = ax + @divTrunc(slope_num, slope_den);

            // If intersection is to the right of point p, toggle inside
            if (px + 1 < intersect_x) {
                inside = !inside;
            }
        }
    }

    return inside;
}

fn part1(allocator: Allocator) anyerror!void {
    const input = @embedFile("puzzle-09");
    std.debug.print("--- INPUT---\n{s}\n------------\n", .{input});

    var it: std.Io.Reader = .fixed(input);
    var tiles: std.array_list.Managed(@Vector(2, u64)) = .init(allocator);
    defer tiles.deinit();

    var last_area: u64 = 0;
    while (try it.takeDelimiter('\n')) |line| {
        if (std.mem.indexOf(u8, line, ",")) |sep| {
            const x = try std.fmt.parseInt(u64, line[0..sep], 10);
            const y = try std.fmt.parseInt(u64, line[sep + 1 ..], 10);
            for (tiles.items) |_| {
                const area = calcArea(x, t[0], y, t[1]);
                last_area = @max(area, last_area);
            }
            try tiles.append(.{ x, y });
        }
    }
    std.debug.print("Largest area: {d}\n", .{last_area});
}

Part 2:
Visualizing my puzzle input in an svg file really helped to understand that the input path is essentially just a big circle like shape with a very wide rectangular hole inside. My code is not really the fastest, but happy I found a working solution :upside_down_face:.

The code for the svg creation can be found in my repo.

Green is the input path, red the final rectangle shape within:

Visualized result

fn part2(allocator: Allocator) anyerror!void {
    const input = @embedFile("puzzle-09");
    std.debug.print("--- INPUT---\n{s}\n------------\n", .{input});

    var it: std.Io.Reader = .fixed(input);
    var tiles: std.array_list.Managed(@Vector(2, u64)) = .init(allocator);
    defer tiles.deinit();

    var largest: @Vector(2, usize) = .{ 0, 0 };
    while (try it.takeDelimiter('\n')) |line| {
        if (std.mem.indexOf(u8, line, ",")) |sep| {
            const x = try std.fmt.parseInt(u64, line[0..sep], 10);
            const y = try std.fmt.parseInt(u64, line[sep + 1 ..], 10);
            if (x > largest[0]) {
                largest[0] = x;
            }
            if (y > largest[1]) {
                largest[1] = y;
            }
            try tiles.append(.{ x, y });
        }
    }

    std.debug.print("{d} x {d}\n", .{ largest[0], largest[1] });

    try svg.init(
        "2025-day09-part2.svg",
        largest[0],
        largest[1],
        0,
        largest[0],
        0,
        largest[1],
    );

    try svg.startPolygon();
    for (tiles.items) |tile| {
        try svg.addPolygonPoint(tile[0], tile[1]);
    }
    try svg.endPolygon("green");

    var path: std.array_list.Managed(@Vector(2, u64)) = .init(allocator);
    defer path.deinit();

    const start = findLeftMostLowest(&tiles);
    try path.append(start);

    std.debug.print("Start: {d}\n", .{start});

    const tile_count = tiles.items.len;
    var current = start;
    var idx: u64 = 1;
    while (true) {
        if (idx >= tile_count) break;
        var closest_idx: usize = 0;
        var closest = tiles.items[closest_idx];
        var d: f64 = 100.0;
        for (1..tiles.items.len) |i| {
            const tile = tiles.items[i];
            if (tile[0] == current[0] and tile[1] == current[1]) continue;
            if (current[0] != tile[0] and current[1] != tile[1]) continue;
            const closest_d = distance(current, tile);
            if (closest_d < d) {
                d = closest_d;
                closest = tile;
                closest_idx = i;
            }
        }
        // map.set(closest[0], closest[1], idx + '0');
        idx += 1;
        current = closest;
        try path.append(tiles.swapRemove(closest_idx));
    }

    var largest_area: u64 = 0;
    var a: @Vector(2, u64) = .{ 0, 0 };
    var b: @Vector(2, u64) = .{ 0, 0 };
    for (0..path.items.len) |i| {
        const p = path.items[i];
        outer: for (0..path.items.len) |j| {
            const q = path.items[j];
            if (p[0] == q[0] and p[1] == q[1]) continue;
            const area = calcArea(p[0], q[0], p[1], q[1]);
            const opposite_a_inside = insidePath(&path, .{ p[0], q[1] });
            const opposite_b_inside = insidePath(&path, .{ q[0], p[1] });
            if (opposite_a_inside and opposite_b_inside and area > largest_area) {
                var all_inside = true;
                
                // This part is really just brute forced and not at all optimized.
                // The below is checking every y coordinate of the rectangle path
                // for if its inside the path or not.
                // There's probably better/faster ways of doing this.
                if (p[1] < q[1]) {
                    for (p[1]..q[1]) |y| {
                        const inside = insidePath(&path, .{ p[0], y });
                        all_inside = all_inside and inside;
                        if (!all_inside) continue :outer;
                    }
                }
                if (q[1] < p[1]) {
                    for (q[1]..p[1]) |y| {
                        const inside = insidePath(&path, .{ p[0], y });
                        all_inside = all_inside and inside;
                        if (!all_inside) continue :outer;
                    }
                }

                if (all_inside) {
                    largest_area = area;
                    a = p;
                    b = q;
                }
            }
        }
    }
    try svg.startPolygon();
    try svg.addPolygonPoint(a[0], a[1]);
    try svg.addPolygonPoint(a[0], b[1]);
    try svg.addPolygonPoint(b[0], b[1]);
    try svg.addPolygonPoint(b[0], a[1]);
    try svg.endPolygon("red");

    try svg.close();

    std.debug.print("Area: {d}\n", .{largest_area});
}