AoC 2024: Day 12

Main thread for Day 12 of the 2024 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.

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.
  2. Have Fun

Day 12 Challenge

Templates:

Resources:

Previous days discussions

Day 1: AoC 2024: Day 1
Day 2: AoC 2024: Day 2
Day 3: AoC 2024: Day 3
Day 4: AoC 2024: Day 4
Day 5: AoC 2024: Day 5
Day 6: AoC 2024: Day 6
Day 7: AoC 2024: Day 7
Day 8: AoC 2024: Day 8
Day 9: AoC 2024: Day 9
Day 10: AoC 2024: Day 10
Day 11: AoC 2024: Day 11

I see zero traffic for this day’s problem. I found it required a lot of thinking before jumping into the code. I found two key observations that helped me:

  • Trivial, useful for parts 1 and 2: Using flood fill to find each region.
  • Sophisticated, vital for part 2: Counting corners is the same as counting straight edges.

Good luck!

3 Likes

Very ugly p2 but i guess it fine for small inputs like this.

Part 1 & 2
const std = @import("std");

const util = @import("util.zig");
const gpa = util.gpa;

const data = @embedFile("data/day12.txt");

const Point = struct { x: usize, y: usize };

const lim = 140;
var grid = [_][lim]u8{[_]u8{undefined} ** lim} ** lim;
var visited = [_][lim]bool{[_]bool{false} ** lim} ** lim;

pub fn main() !void {
    var p1: u32 = 0;
    var p2: u32 = 0;

    var idx: usize = 0;
    var lines = std.mem.tokenizeScalar(u8, data, '\n');
    while (lines.next()) |line| : (idx += 1) {
        grid[idx] = line[0..lim].*;
    }

    for (0..lim) |i| {
        for (0..lim) |j| {
            const res = try region_cost(i, j);
            p1 += res.p1;
            p2 += res.p2;
        }
    }

    std.debug.print("part1: {}\n", .{p1});
    std.debug.print("part2: {}\n", .{p2});
}

fn region_cost(x_start: usize, y_start: usize) !struct { p1: u32, p2: u32 } {
    if (visited[x_start][y_start]) return .{ .p1 = 0, .p2 = 0 };

    var stack = std.ArrayList(Point).init(gpa);
    try stack.append(Point{ .x = x_start, .y = y_start });
    const char = grid[x_start][y_start];
    visited[x_start][y_start] = true;

    var area: u32 = 0;
    var borders_count: u32 = 0;

    var borders: [4]std.ArrayList(Point) = undefined;
    for (0..4) |i| {
        borders[i] = std.ArrayList(Point).init(gpa);
    }

    while (stack.items.len != 0) {
        const current = stack.pop();
        const x = current.x;
        const y = current.y;
        area += 1;
        borders_count += try check_borders(x, y, char, &borders);

        if (x > 0 and grid[x - 1][y] == char and !visited[x - 1][y]) {
            visited[x - 1][y] = true;
            try stack.append(.{ .x = x - 1, .y = y });
        }

        if (x < lim - 1 and grid[x + 1][y] == char and !visited[x + 1][y]) {
            visited[x + 1][y] = true;
            try stack.append(Point{ .x = x + 1, .y = y });
        }

        if (y > 0 and grid[x][y - 1] == char and !visited[x][y - 1]) {
            visited[x][y - 1] = true;
            try stack.append(Point{ .x = x, .y = y - 1 });
        }

        if (y < lim - 1 and grid[x][y + 1] == char and !visited[x][y + 1]) {
            visited[x][y + 1] = true;
            try stack.append(Point{ .x = x, .y = y + 1 });
        }
    }

    std.mem.sort(Point, borders[0].items, {}, cmp_y);
    std.mem.sort(Point, borders[0].items, {}, cmp_x);
    std.mem.sort(Point, borders[1].items, {}, cmp_y);
    std.mem.sort(Point, borders[1].items, {}, cmp_x);
    std.mem.sort(Point, borders[2].items, {}, cmp_x);
    std.mem.sort(Point, borders[2].items, {}, cmp_y);
    std.mem.sort(Point, borders[3].items, {}, cmp_x);
    std.mem.sort(Point, borders[3].items, {}, cmp_y);

    var sides: u32 = 4;
    for (0..4) |i| {
        for (0..borders[i].items.len - 1) |j| {
            if (i < 2) {
                if (borders[i].items[j].y + 1 != borders[i].items[j + 1].y or borders[i].items[j].x != borders[i].items[j + 1].x)
                    sides += 1;
            } else {
                if (borders[i].items[j].x + 1 != borders[i].items[j + 1].x or borders[i].items[j].y != borders[i].items[j + 1].y)
                    sides += 1;
            }
        }
    }

    return .{ .p1 = area * borders_count, .p2 = area * sides };
}

fn check_borders(x: usize, y: usize, char: u8, borders: *[4]std.ArrayList(Point)) !u32 {
    var borders_count: u32 = 0;

    if (x == 0 or grid[x - 1][y] != char) {
        try borders[0].append(Point{ .x = x, .y = y });
        borders_count += 1;
    }
    if (x == lim - 1 or grid[x + 1][y] != char) {
        try borders[1].append(Point{ .x = x, .y = y });
        borders_count += 1;
    }
    if (y == 0 or grid[x][y - 1] != char) {
        try borders[2].append(Point{ .x = x, .y = y });
        borders_count += 1;
    }
    if (y == lim - 1 or grid[x][y + 1] != char) {
        try borders[3].append(Point{ .x = x, .y = y });
        borders_count += 1;
    }

    return borders_count;
}

fn cmp_x(_: void, a: Point, b: Point) bool {
    return a.x < b.x;
}

fn cmp_y(_: void, a: Point, b: Point) bool {
    return a.y < b.y;
}
1 Like

Starting to get a little trickier, or I’m just slow today. There is a more convenient way to write the if statements for part 2, for sure.

https://zigbin.io/5a88d7

1 Like

Part two was tough! First, I wanted to walk around the edge of each region but it looked like a huge headache. So instead I thought I could scan the whole map line by line to detect horizontal borders, then vertically to detect vertical borders.

1 Like

I could probably simplify the if chains. Its ugly, but it works.

https://zigbin.io/5d3ef1

1 Like

450us. I tried my best to make the code as understandable as possible. For part 2 I tried to focus on finding the different ways a fence can be not the first one of a straight line when going clockwise. So the whole thing is just one pass. .

I don’t know if I am the only one, but I got into the habit of naming all my loops so it is very clear from what I am breaking or continuing.

Also at the end of a block I just call something like continue :loop_on_directions so I know where I am and have no headaches when closing a bunch of braces. I am not afraid of going indentation deep (main is all you need), but at the same time try to break early to return left as soon as possible. Enough rambling, see you tomorrow…

https://zigbin.io/e4e04e