AoC 2024: Day 6

Main thread for Day 6 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 6 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

1 Like

Pretty happy with my solution for now but there has to be a more efficient way for p2 that i didn’t see.

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

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

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

const Direction = enum { up, down, left, right };
const Point = struct { x: usize, y: usize };
const Guard = struct {
    x: usize,
    y: usize,
    outside: bool = false,
    direction: Direction = Direction.up,
    fn rotate(self: *Guard) void {
        switch (self.direction) {
            .up => self.direction = .right,
            .down => self.direction = .left,
            .left => self.direction = .up,
            .right => self.direction = .down,
        }
    }
};
const lim: usize = 130;

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

    var map = [_][lim]u8{[_]u8{0} ** lim} ** lim;
    var lines = std.mem.tokenizeScalar(u8, data, '\n');
    var i: usize = 0;
    var guard_found = false;
    var guard: Guard = undefined;

    while (lines.next()) |line| : (i += 1) {
        if (guard_found) {
            map[i] = line[0..lim].*;
            continue;
        }

        for (line, 0..) |c, j| {
            if (c == '^') {
                guard = Guard{ .x = i, .y = j };
                guard_found = true;
            }

            map[i][j] = c;
        }
    }

    var visited_points = std.ArrayList(Point).init(gpa);
    const initial_pos = Point{ .x = guard.x, .y = guard.y };

    while (!guard.outside) {
        if (move(&map, &guard)) |point| {
            p1 += 1;
            try visited_points.append(point);
        }
    }

    for (visited_points.items) |point| {
        var map_modified = map;
        map_modified[point.x][point.y] = '#';
        var visitied_times = [_][lim]u8{[_]u8{0} ** lim} ** lim;
        guard = Guard{ .x = initial_pos.x, .y = initial_pos.y };
        var previous_pos = Point{ .x = initial_pos.x, .y = initial_pos.y };

        while (!guard.outside) {
            _ = move(&map_modified, &guard);

            if (previous_pos.x != guard.x or previous_pos.y != guard.y) {
                visitied_times[previous_pos.x][previous_pos.y] += 1;
                previous_pos = Point{ .x = guard.x, .y = guard.y };
            }

            if (visitied_times[previous_pos.x][previous_pos.y] > 2) {
                p2 += 1;
                break;
            }
        }
    }

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

fn move(map: *[lim][lim]u8, guard: *Guard) ?Point {
    var point: ?Point = null;
    if (map[guard.x][guard.y] == '.') {
        map[guard.x][guard.y] = 'X';
        point = Point{ .x = guard.x, .y = guard.y };
    }

    var x = guard.x;
    var y = guard.y;

    switch (guard.direction) {
        .up => {
            if (guard.x == 0) {
                guard.outside = true;
            } else x -= 1;
        },
        .down => {
            if (guard.x == lim - 1) {
                guard.outside = true;
            } else x += 1;
        },
        .left => {
            if (guard.y == 0) {
                guard.outside = true;
            } else y -= 1;
        },
        .right => {
            if (guard.y == lim - 1) {
                guard.outside = true;
            } else y += 1;
        },
    }

    if (!guard.outside and map[x][y] == '#') {
        guard.rotate();
    } else {
        guard.x = x;
        guard.y = y;
    }

    return point;
}

This was fun today.
But boy, did the code get ugly. Works though.

Solution 1
const std = @import("std");

//const input = @embedFile("./small.inp");
const input = @embedFile("./big.inp");

const direction = enum { north, east, south, west };
const guard_t = struct {
    dir: direction,
    irow: usize,
    icol: usize,
    ingrid: bool,

    fn rotate(self: *guard_t) void {
        switch (self.dir) {
            .north => self.dir = .east,
            .east => self.dir = .south,
            .south => self.dir = .west,
            .west => self.dir = .north,
        }
    }

    fn move(self: *guard_t, grid: *std.ArrayList(std.ArrayList(u8))) void {
        // check if next step would get outside
        if ((self.irow == 0 and self.dir == .north) or // north
            (self.irow == grid.items.len - 1 and self.dir == .south) or // south
            (self.icol == 0 and self.dir == .west) or // west
            (self.icol == grid.items[1].items.len - 1 and self.dir == .east))
        {
            grid.items[self.irow].items[self.icol] = 'X';
            self.ingrid = false;
            return;
        }

        // check if blocked and reorientate
        var blocked = true;
        while (blocked) {
            if (self.dir == .north and grid.items[self.irow - 1].items[self.icol] == '#') {
                self.rotate();
            } else if (self.dir == .south and grid.items[self.irow + 1].items[self.icol] == '#') {
                self.rotate();
            } else if (self.dir == .west and grid.items[self.irow].items[self.icol - 1] == '#') {
                self.rotate();
            } else if (self.dir == .east and grid.items[self.irow].items[self.icol + 1] == '#') {
                self.rotate();
            } else {
                blocked = false;
            }
        }

        // move one step
        grid.items[self.irow].items[self.icol] = 'X';
        if (self.dir == .north) {
            self.irow -= 1;
        } else if (self.dir == .south) {
            self.irow += 1;
        } else if (self.dir == .west) {
            self.icol -= 1;
        } else if (self.dir == .east) {
            self.icol += 1;
        }

        // mark on map
        if (self.dir == .north) {
            grid.items[self.irow].items[self.icol] = '^';
        } else if (self.dir == .south) {
            grid.items[self.irow].items[self.icol] = 'v';
        } else if (self.dir == .west) {
            grid.items[self.irow].items[self.icol] = '<';
        } else if (self.dir == .east) {
            grid.items[self.irow].items[self.icol] = '>';
        }
    }
};

fn print_grid(grid: std.ArrayList(std.ArrayList(u8))) void {
    std.debug.print("\n", .{});
    for (grid.items) |line| {
        for (line.items) |char| {
            std.debug.print("{c}", .{char});
        }
        std.debug.print("\n", .{});
    }
}

fn get_num_visited(grid: std.ArrayList(std.ArrayList(u8))) i32 {
    var nvisited: i32 = 0;
    for (grid.items, 0..) |row, irow| {
        for (row.items, 0..) |_, icol| {
            if (grid.items[irow].items[icol] == 'X') {
                nvisited += 1;
            }
        }
    }
    return nvisited;
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var grid = std.ArrayList(std.ArrayList(u8)).init(allocator);
    defer _ = grid.deinit();

    var lines = std.mem.tokenizeScalar(u8, input, '\n');
    while (lines.next()) |line| {
        var row = std.ArrayList(u8).init(allocator);
        for (line) |char| {
            try row.append(char);
        }
        try grid.append(row);
    }

    var guard = guard_t{
        .dir = .north,
        .irow = 0,
        .icol = 0,
        .ingrid = true,
    };

    //find position of guard
    outer: for (grid.items, 0..) |row, irow| {
        for (row.items, 0..) |_, icol| {
            if (grid.items[irow].items[icol] == '^') {
                guard.irow = irow;
                guard.icol = icol;
                break :outer;
            }
        }
    }

    std.debug.print("guard at {d} {d}\n", .{ guard.irow, guard.icol });
    print_grid(grid);
    while (guard.ingrid) {
        guard.move(&grid);
        //print_grid(grid);
    }
    print_grid(grid);

    std.debug.print("Number of fields visited by guard: {d}\n", .{get_num_visited(grid)});
    //
    // cleanup of individual page_lists
    for (grid.items) |row| {
        _ = row.deinit();
    }
}
Solution 2
const std = @import("std");

//const input = @embedFile("./small.inp");
const input = @embedFile("./big.inp");

const direction = enum { north, east, south, west };
const guard_t = struct {
    dir: direction,
    irow: usize,
    icol: usize,
    ingrid: bool,
    inloop: bool,

    fn rotate(self: *guard_t) void {
        switch (self.dir) {
            .north => self.dir = .east,
            .east => self.dir = .south,
            .south => self.dir = .west,
            .west => self.dir = .north,
        }
    }

    fn place_marker(self: *guard_t, grid: *std.ArrayList(std.ArrayList([5]u8))) void {
        if (grid.items[self.irow].items[self.icol][0] == '-' and self.dir == .north) {
            grid.items[self.irow].items[self.icol][0] = '+';
            grid.items[self.irow].items[self.icol][1] = 1;
        } else if (grid.items[self.irow].items[self.icol][0] == '-' and self.dir == .south) {
            grid.items[self.irow].items[self.icol][0] = '+';
            grid.items[self.irow].items[self.icol][3] = 1;
        } else if (grid.items[self.irow].items[self.icol][0] == '|' and self.dir == .west) {
            grid.items[self.irow].items[self.icol][0] = '+';
            grid.items[self.irow].items[self.icol][4] = 1;
        } else if (grid.items[self.irow].items[self.icol][0] == '|' and self.dir == .east) {
            grid.items[self.irow].items[self.icol][0] = '+';
            grid.items[self.irow].items[self.icol][2] = 1;
        } else if (self.dir == .north) {
            grid.items[self.irow].items[self.icol][0] = '|';
            grid.items[self.irow].items[self.icol][1] = 1;
        } else if (self.dir == .south) {
            grid.items[self.irow].items[self.icol][0] = '|';
            grid.items[self.irow].items[self.icol][3] = 1;
        } else if (self.dir == .west) {
            grid.items[self.irow].items[self.icol][0] = '-';
            grid.items[self.irow].items[self.icol][4] = 1;
        } else if (self.dir == .east) {
            grid.items[self.irow].items[self.icol][0] = '-';
            grid.items[self.irow].items[self.icol][2] = 1;
        }
    }

    fn move(self: *guard_t, grid: *std.ArrayList(std.ArrayList([5]u8))) void {
        // check if next step would get outside
        if (self.irow == 0 and self.dir == .north) {
            self.place_marker(grid);
            self.ingrid = false;
            return;
        }
        if (self.irow == grid.items.len - 1 and self.dir == .south) {
            self.place_marker(grid);
            self.ingrid = false;
            return;
        }
        if (self.icol == 0 and self.dir == .west) {
            self.place_marker(grid);
            self.ingrid = false;
            return;
        }
        if (self.icol == grid.items[self.irow].items.len - 1 and self.dir == .east) {
            self.place_marker(grid);
            self.ingrid = false;
            return;
        }

        // check if blocked and reorientate
        var blocked = true;
        while (blocked) {
            if (self.dir == .north and grid.items[self.irow - 1].items[self.icol][0] == '#') {
                self.place_marker(grid);
                self.rotate();
            } else if (self.dir == .south and grid.items[self.irow + 1].items[self.icol][0] == '#') {
                self.place_marker(grid);
                self.rotate();
            } else if (self.dir == .west and grid.items[self.irow].items[self.icol - 1][0] == '#') {
                self.place_marker(grid);
                self.rotate();
            } else if (self.dir == .east and grid.items[self.irow].items[self.icol + 1][0] == '#') {
                self.place_marker(grid);
                self.rotate();
            } else {
                blocked = false;
            }
        }

        // check if in a loop
        if ((grid.items[self.irow].items[self.icol][1] == 1 and self.dir == .north) or
            (grid.items[self.irow].items[self.icol][2] == 1 and self.dir == .east) or
            (grid.items[self.irow].items[self.icol][3] == 1 and self.dir == .south) or
            (grid.items[self.irow].items[self.icol][4] == 1 and self.dir == .west))
        {
            self.inloop = true;
            return;
        }

        // move one step
        self.place_marker(grid);
        if (self.dir == .north) {
            self.irow -= 1;
        } else if (self.dir == .south) {
            self.irow += 1;
        } else if (self.dir == .west) {
            self.icol -= 1;
        } else if (self.dir == .east) {
            self.icol += 1;
        }
    }
};

fn print_grid(grid: std.ArrayList(std.ArrayList([5]u8))) void {
    std.debug.print("\n", .{});
    for (grid.items) |line| {
        for (line.items) |char| {
            std.debug.print("{c}", .{char[0]});
        }
        std.debug.print("\n", .{});
    }
}

fn get_num_visited(grid: std.ArrayList(std.ArrayList(u8))) i32 {
    var nvisited: i32 = 0;
    for (grid.items, 0..) |row, irow| {
        for (row.items, 0..) |_, icol| {
            if (grid.items[irow].items[icol] == 'X') {
                nvisited += 1;
            }
        }
    }
    return nvisited;
}

fn reread_grid(grid: *std.ArrayList(std.ArrayList([5]u8))) !void {
    var lines = std.mem.tokenizeScalar(u8, input, '\n');
    var irow: usize = 0;
    while (lines.next()) |line| {
        for (line, 0..) |char, icol| {
            grid.items[irow].items[icol] = [5]u8{ char, 0, 0, 0, 0 };
        }
        irow += 1;
    }
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var grid = std.ArrayList(std.ArrayList([5]u8)).init(allocator);
    defer _ = grid.deinit();

    var lines = std.mem.tokenizeScalar(u8, input, '\n');
    while (lines.next()) |line| {
        var row = std.ArrayList([5]u8).init(allocator);
        for (line) |char| {
            try row.append([5]u8{ char, 0, 0, 0, 0 });
        }
        try grid.append(row);
    }

    var guard = guard_t{
        .dir = .north,
        .irow = 0,
        .icol = 0,
        .ingrid = true,
        .inloop = false,
    };

    var nbarriers: i32 = 0;
    //print_grid(grid);
    for (0..grid.items.len) |irow| {
        for (0..grid.items[irow].items.len) |icol| {
            if (grid.items[irow].items[icol][0] != '#' and grid.items[irow].items[icol][0] != '^') {
                try reread_grid(&grid);
                // reset guard
                guard.ingrid = true;
                guard.inloop = false;
                guard.dir = .north;
                //find position of guard
                outer: for (grid.items, 0..) |row, jrow| {
                    for (row.items, 0..) |_, jcol| {
                        if (grid.items[jrow].items[jcol][0] == '^') {
                            guard.irow = jrow;
                            guard.icol = jcol;
                            break :outer;
                        }
                    }
                }

                grid.items[irow].items[icol][0] = '#';

                while (guard.ingrid and !guard.inloop) {
                    guard.move(&grid);
                    //print_grid(grid);
                }
                if (guard.inloop) {
                    //print_grid(grid);
                    //std.debug.print("inloop\n", .{});
                    nbarriers += 1;
                }
            }
        }
    }

    std.debug.print("Number of valid barriers: {d}\n", .{nbarriers});

    // cleanup of individual page_lists
    for (grid.items) |row| {
        _ = row.deinit();
    }
}

Always nice when you don’t need heap allocations, but there has to be a better way for part 2 than just brute forcing it.

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

const Point = struct {
    col: usize,
    row: usize,
    fn move(self: Point, direction: Direction) Point {
        return switch (direction) {
            .up => Point{ .col = self.col, .row = self.row - 1 },
            .down => Point{ .col = self.col, .row = self.row + 1 },
            .left => Point{ .col = self.col - 1, .row = self.row },
            .right => Point{ .col = self.col + 1, .row = self.row },
        };
    }
};

const Direction = enum(u8) {
    up = 0b1,
    down = 0b10,
    left = 0b100,
    right = 0b1000,
    pub fn turn(self: *Direction) void {
        switch (self.*) {
            .up => self.* = .right,
            .right => self.* = .down,
            .down => self.* = .left,
            .left => self.* = .up,
        }
    }
};

pub fn part1(input: []const u8, comptime rows: usize, comptime cols: usize) u32 {
    var grid: [rows][cols]u8 = undefined;
    var it = std.mem.tokenizeScalar(u8, input, '\n');
    var i: usize = 0;
    var pos: Point = undefined;
    var direction: Direction = .up;
    while (it.next()) |line| : (i += 1) {
        std.mem.copyForwards(u8, &grid[i], line);
        var ii: usize = 0;
        for (line) |c| {
            if (c == '^') {
                pos = Point{ .col = ii, .row = i };
            }
            ii += 1;
        }
    }
    while (pos.col > 0 and pos.col < cols - 1 and pos.row < rows - 1 and pos.row > 0) {
        const tmp = pos.move(direction);
        if (grid[tmp.row][tmp.col] == '#') {
            direction.turn();
        } else {
            grid[tmp.row][tmp.col] = 'X';
            pos = tmp;
        }
    }
    var acc: u32 = 0;
    for (grid) |row| {
        for (row) |c| {
            if (c == 'X') {
                acc += 1;
            }
        }
    }
    return acc;
}

pub fn part2(input: []const u8, comptime rows: usize, comptime cols: usize) u32 {
    var start_grid: [rows][cols]u8 = undefined;
    var it = std.mem.tokenizeScalar(u8, input, '\n');
    var i: usize = 0;
    var start_pos: Point = undefined;
    const start_direction: Direction = .up;
    while (it.next()) |line| : (i += 1) {
        var ii: usize = 0;
        for (line) |c| {
            if (c == '^') {
                start_pos = Point{ .col = ii, .row = i };
            }
            if (c == '#') {
                start_grid[i][ii] = '#';
            } else {
                start_grid[i][ii] = 0;
            }
            ii += 1;
        }
    }
    i = 0;
    var acc: u32 = 0;
    while (i < rows) : (i += 1) {
        var ii: usize = 0;
        outer: while (ii < cols) : (ii += 1) {
            var grid = start_grid;
            grid[i][ii] = '#';
            var pos = start_pos;
            var direction = start_direction;
            while (pos.col > 0 and pos.col < cols - 1 and pos.row < rows - 1 and pos.row > 0) {
                const tmp = pos.move(direction);
                if (grid[tmp.row][tmp.col] == '#') {
                    direction.turn();
                } else {
                    if (grid[tmp.row][tmp.col] & @intFromEnum(direction) > 0) {
                        acc += 1;
                        continue :outer;
                    }
                    grid[tmp.row][tmp.col] |= @intFromEnum(direction);
                    pos = tmp;
                }
            }
        }
    }
    return acc;
}

Woof, part two was rough for me.
Thanks for the tip of tracking the visited cells and only checking those in part 2

Parts 1 & 2
const std = @import("std");
const log = std.log.scoped(.day06);

const test_data =
    \\....#.....
    \\.........#
    \\..........
    \\..#.......
    \\.......#..
    \\..........
    \\.#..^.....
    \\........#.
    \\#.........
    \\......#...
;

const actual_data = @embedFile("./day06.txt");

// const data = test_data;
const data = actual_data;

pub fn main() !void {
    var seen = [_]bool{false} ** data.len;
    const part1_result = part1(&seen);
    const part2_result = part2(&seen);
    const stdout = std.io.getStdOut().writer();
    try stdout.print("Part 1: {d}\n", .{part1_result});
    try stdout.print("Part 2: {d}\n", .{part2_result});
}

const Direction = enum(u8) {
    up = 0b0001,
    down = 0b0010,
    left = 0b0100,
    right = 0b1000,
};

pub fn part1(seen: []bool) usize {
    const map = Map.init(data);
    var guard = Guard.init(map);
    var total_tiles: usize = 0;

    while (true) {
        const gi = guard.row * map.stride + guard.col;
        if (gi < seen.len) {
            if (!seen[gi]) {
                total_tiles += 1;
            }
            seen[gi] = true;
        }
        guard.move(map) catch break;
    }

    return total_tiles;
}

const Map = struct {
    raw: []const u8,
    stride: usize,
    row_count: usize,

    pub fn init(raw: []const u8) Map {
        const stride = std.mem.indexOf(u8, raw, "\n").? + 1;
        const row_count = (raw.len / stride) + 1;
        log.debug("Total Rows: {}, Cols: {}", .{ row_count, stride });
        return .{ .raw = raw, .stride = stride, .row_count = row_count };
    }

    pub fn get(self: Map, row: usize, col: usize) u8 {
        const i = row * self.stride + col;
        if (i >= self.raw.len) {
            return '?';
        }
        return self.raw[i];
    }
};

const Guard = struct {
    row: usize,
    col: usize,
    direction: Direction,

    pub fn init(map: Map) Guard {
        const loc_raw = std.mem.indexOf(u8, map.raw, "^").?;
        const row = loc_raw / map.stride;
        const col = loc_raw % map.stride;
        return .{ .row = row, .col = col, .direction = .up };
    }

    pub fn move(self: *Guard, map: Map) !void {
        switch (self.direction) {
            .up => {
                if (self.row == 0) {
                    return error.OutOfBounds;
                }
                if (map.get(self.row - 1, self.col) == '#') {
                    self.turn();
                } else {
                    self.row -= 1;
                }
            },
            .down => {
                if (map.get(self.row + 1, self.col) == '#') {
                    self.turn();
                } else {
                    self.row += 1;
                    if (self.row == map.row_count) {
                        return error.OutOfBounds;
                    }
                }
            },
            .left => {
                if (self.col == 0) {
                    return error.OutOfBounds;
                }
                if (map.get(self.row, self.col - 1) == '#') {
                    self.turn();
                } else {
                    self.col -= 1;
                }
            },
            .right => {
                if (map.get(self.row, self.col + 1) == '#') {
                    self.turn();
                } else {
                    self.col += 1;
                    if (self.col == map.stride) {
                        return error.OutOfBounds;
                    }
                }
            },
        }
    }

    pub fn turn(self: *Guard) void {
        switch (self.direction) {
            .up => {
                self.direction = .right;
            },
            .down => {
                self.direction = .left;
            },
            .left => {
                self.direction = .up;
            },
            .right => {
                self.direction = .down;
            },
        }
    }
};

test "rotations" {
    const test_map =
        \\..#.
        \\#..#
        \\..^.
        \\.#..
    ;
    const map = Map.init(test_map);
    var guard = Guard.init(map);

    try std.testing.expectEqualDeep(Guard{ .row = 2, .col = 2, .direction = .up }, guard);

    try guard.move(map);
    try std.testing.expectEqualDeep(Guard{ .row = 1, .col = 2, .direction = .up }, guard);

    try guard.move(map);
    try std.testing.expectEqualDeep(Guard{ .row = 1, .col = 2, .direction = .right }, guard);

    try guard.move(map);
    try std.testing.expectEqualDeep(Guard{ .row = 1, .col = 2, .direction = .down }, guard);

    guard.row = 2;
    guard.col = 1;
    try guard.move(map);
    try std.testing.expectEqualDeep(Guard{ .row = 2, .col = 1, .direction = .left }, guard);
}
test "out of bounds" {
    const test_map =
        \\..#.
        \\#..#
        \\..^.
        \\.#..
    ;
    const map = Map.init(test_map);
    try std.testing.expectEqualDeep(Map{ .raw = test_map, .row_count = 4, .stride = 5 }, map);
    var guard = Guard.init(map);

    guard.row = 0;
    try std.testing.expectError(error.OutOfBounds, guard.move(map));

    guard.row = 3;
    guard.direction = .down;
    try std.testing.expectError(error.OutOfBounds, guard.move(map));

    guard.col = 0;
    guard.direction = .left;
    try std.testing.expectError(error.OutOfBounds, guard.move(map));

    guard.col = 4;
    guard.direction = .right;
    try std.testing.expectError(error.OutOfBounds, guard.move(map));
}
fn part2(seen: []bool) usize {
    var total_loops: usize = 0;
    log.debug("Seen: {any}", .{seen});

    var scratch = [_]u8{0} ** data.len;
    next_check: for (seen, 0..) |s, new_item| {
        if (s and data[new_item] != '^') {
            var directions = [_]u8{0} ** data.len;
            @memcpy(scratch[0..data.len], data);
            scratch[new_item] = '#';
            const map = Map.init(&scratch);
            log.debug("Checking with {}, {}", .{ new_item / map.stride, new_item % map.stride });

            var guard = Guard.init(map);

            while (true) {
                const i = guard.row * map.stride + guard.col;
                if (i >= seen.len) {
                    break;
                }
                switch (guard.direction) {
                    .up, .down => |dir| {
                        switch (scratch[i]) {
                            '-' => {
                                scratch[i] = '+';
                                directions[i] |= @intFromEnum(dir);
                            },
                            '+', '|' => {
                                if (directions[i] & @intFromEnum(dir) != 0) {
                                    total_loops += 1;
                                    continue :next_check;
                                }
                                directions[i] |= @intFromEnum(dir);
                            },
                            '.' => {
                                scratch[i] = '|';
                                directions[i] |= @intFromEnum(dir);
                            },
                            else => |char| {
                                log.err("Found an unexpected cell: {c}", .{char});
                            },
                        }
                    },
                    .left, .right => |dir| {
                        switch (scratch[i]) {
                            '|' => {
                                scratch[i] = '+';
                                directions[i] |= @intFromEnum(dir);
                            },
                            '+', '-' => {
                                if (directions[i] & @intFromEnum(dir) != 0) {
                                    total_loops += 1;
                                    continue :next_check;
                                }
                                directions[i] |= @intFromEnum(dir);
                            },
                            '.' => {
                                scratch[i] = '-';
                                directions[i] |= @intFromEnum(dir);
                            },
                            else => {
                                log.err("Found an unexpected cell: {c}", .{scratch[i]});
                            },
                        }
                    },
                }
                guard.move(map) catch {
                    if (data.ptr == test_data.ptr) {
                        for (0..map.row_count) |row| {
                            for (0..map.stride) |col| {
                                const index = row * map.stride + col;
                                if (index >= scratch.len) {
                                    std.debug.print("{c}", .{'?'});
                                } else {
                                    const char = scratch[index];
                                    std.debug.print("{c}", .{char});
                                }
                            }
                            std.debug.print("\n", .{});
                        }
                    }
                    continue :next_check;
                };
            }
        }
    }

    return total_loops;
}

Damn, part 1 was okay, part 2 was rough for me too. I tried two clever approaches that didn’t work out before brute forcing it.

But then my bruteforce had an infinite loop bug, so I resorted to a reasonably high upper bound to say “meh, I guess I’m in a loop”. I feel dirty.

const std = @import("std");
const u = @import("util.zig");

const file = @embedFile("data/day06.txt");
var data: [file.len]u8 = undefined;

const line_length = 131; //130x130

const Dir = enum(u8) {
    up = '^',
    down = 'v',
    left = '<',
    right = '>',

    fn init(c: u8) ?Dir {
        return switch (c) {
            @intFromEnum(Dir.up) => Dir.up,
            @intFromEnum(Dir.down) => Dir.down,
            @intFromEnum(Dir.left) => Dir.left,
            @intFromEnum(Dir.right) => Dir.right,
            inline else => null,
        };
    }
};

fn toIndex(row: usize, col: usize) usize {
    return @as(usize, row) * line_length + @as(usize, col);
}

fn toCoords(index: usize) struct { row: usize, col: usize } {
    const row = index / line_length;
    const col = index - row * line_length;
    return .{ .row = row, .col = col };
}

// turn right
fn turn(dir: Dir) Dir {
    return switch (dir) {
        .up => .right,
        .right => .down,
        .down => .left,
        .left => .up,
    };
}

// index of next step in a given direction
fn next(index: usize, dir: Dir) ?usize {
    const pos = toCoords(index);
    const max = 129;
    return switch (dir) {
        // top border
        .up => if (pos.row == 0) null else toIndex(pos.row - 1, pos.col),
        // bottom border
        .down => if (pos.row == max) null else toIndex(pos.row + 1, pos.col),
        // left border
        .left => if (pos.col == 0) null else toIndex(pos.row, pos.col - 1),
        // right border
        .right => if (pos.col == max) null else toIndex(pos.row, pos.col + 1),
    };
}

const Guard = struct {
    index: usize,
    dir: Dir,

    const Self = @This();

    fn init() Self {
        return Guard{
            .index = std.mem.indexOfScalar(u8, &data, '^') orelse unreachable,
            .dir = .up,
        };
    }

    // move or return false when out of the grid
    fn step(self: *Self) bool {
        // mark current
        data[self.index] = @intFromEnum(self.dir);
        // attempt to move
        if (next(self.index, self.dir)) |index| {
            if (data[index] == '#') {
                self.dir = turn(self.dir);
            } else {
                self.index = index;
            }
            // keep going
            return true;
        } else {
            // out of the grid
            return false;
        }
    }

    fn loop(self: *Self) bool {
        var count: usize = 0;
        while (true) {
            if (data[self.index] == @intFromEnum(self.dir))
                return true; // found a loop
            // mark current
            data[self.index] = @intFromEnum(self.dir);
            // attempt to move
            if (next(self.index, self.dir)) |index| {
                if (data[index] == '#') {
                    // turn
                    self.dir = turn(self.dir);
                } else {
                    // move forward
                    self.index = index;
                }
            } else {
                // out of the grid
                return false;
            }
            count += 1;
            if (count > 10000) return true;
        }
    }
};

fn level1() !void {
    @memcpy(&data, file);
    var guard = Guard.init();
    while (guard.step()) {}
    var count: usize = 0;
    for (data) |d| {
        if (d == '^' or d == 'v' or d == '<' or d == '>') count += 1;
    }
    u.print("{s}\n", .{data});
    u.print("{}\n", .{count});
}

fn level2() !void {
    var guard: Guard = undefined;
    var count: usize = 0;
    for (0..130) |row| {
        for (0..130) |col| {
            u.print("{} {}\n", .{ row, col });
            @memcpy(&data, file);
            guard = Guard.init();
            // skip the initial position of the guard
            if (toIndex(row, col) == guard.index) continue;
            // remove initial mark
            data[guard.index] = '.';
            // add obstacle
            data[toIndex(row, col)] = '#';
            if (guard.loop()) {
                count += 1;
            }
        }
    }
    u.print("{}\n", .{count});
}

pub fn main() !void {
    // try level1();
    try level2();
}

// Generated from template/template.zig.
// Run `zig build generate` to update.
// Only unmodified days will be updated.

runs in 170ms release fast on my machine. https://zigbin.io/a1391d

cleaned up a lot. first passing part2 took around a minute.

$ poop '/tmp/aoc-exe 06.txt'
Benchmark 1 (30 runs): /tmp/aoc-exe 06.txt
  measurement          mean ± σ            min … max           outliers
  wall_time           169ms ± 1.54ms     166ms …  172ms          0 ( 0%)

i noticed that keeping the grid as a list of slices was much faster than a sparse map of obstacle positions for this one.

Hello, I am learning Zig by doing AoC, I struggled with part 2, but I think I came up with a pretty nice solution (still (semi-)brute force).
I am using this template btw: GitHub - Tomcat-42/aoc.zig-template: Advent of Code Zig Template

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

input: []const u8,
allocator: mem.Allocator,

const Type = union(enum) {
    empty,
    visited,
    blocked,
    bounced: u8,
};
const Dir = enum {
    UP,
    RIGHT,
    DOWN,
    LEFT,

    fn rotate(self: Dir) Dir {
        return switch (self) {
            .UP => .RIGHT,
            .RIGHT => .DOWN,
            .DOWN => .LEFT,
            .LEFT => .UP,
        };
    }

    fn toBit(self: Dir) u8 {
        return switch (self) {
            .UP => 0b1,
            .RIGHT => 0b10,
            .DOWN => 0b100,
            .LEFT => 0b1000,
        };
    }
};
pub fn part1(this: *const @This()) !?i64 {
    var grid = [_][130]Type{[_]Type{.empty} ** 130} ** 130;
    var r: usize = 0;
    var c: usize = 0;
    var it = mem.tokenizeScalar(u8, this.input, '\n');
    var i: usize = 0;
    var dir = Dir.UP;
    while (it.next()) |row| : (i += 1) {
        for (row, 0..) |cell, j| {
            if (cell == '#') {
                grid[i][j] = .blocked;
            } else if (cell == '^') {
                grid[i][j] = .visited;
                r = i;
                c = j;
            }
        }
    }
    var res: i64 = 1;
    while (moveForward(&r, &c, dir)) {
        if (grid[r][c] == .empty) {
            res += 1;
            grid[r][c] = .visited;
        } else if (grid[r][c] == .blocked) {
            moveBackward(&r, &c, dir);
            dir = dir.rotate();
        }
    }

    return res;
}

fn moveForward(r: *usize, c: *usize, dir: Dir) bool {
    switch (dir) {
        .UP => {
            if (r.* == 0) return false;
            r.* -= 1;
        },
        .RIGHT => {
            if (c.* == 129) return false;
            c.* += 1;
        },
        .DOWN => {
            if (r.* == 129) return false;
            r.* += 1;
        },
        .LEFT => {
            if (c.* == 0) return false;
            c.* -= 1;
        },
    }
    return true;
}

fn moveBackward(r: *usize, c: *usize, dir: Dir) void {
    switch (dir) {
        .UP => {
            r.* += 1;
        },
        .RIGHT => {
            c.* -= 1;
        },
        .DOWN => {
            r.* -= 1;
        },
        .LEFT => {
            c.* += 1;
        },
    }
}

// if you hit the same side of a block twice you should be in loop right?
pub fn part2(this: *const @This()) !?i64 {
    var grid = [_][130]Type{[_]Type{.empty} ** 130} ** 130;
    var r: usize = 0;
    var c: usize = 0;

    var it = mem.tokenizeScalar(u8, this.input, '\n');
    var i: usize = 0;
    var dir = Dir.UP;

    while (it.next()) |row| : (i += 1) {
        for (row, 0..) |cell, j| {
            if (cell == '#') {
                grid[i][j] = .blocked;
            } else if (cell == '^') {
                grid[i][j] = .visited;
                r = i;
                c = j;
            }
        }
    }

    var res: i64 = 0;
    while (moveForward(&r, &c, dir)) {
        if (grid[r][c] == .empty) {
            grid[r][c] = .blocked;
            moveBackward(&r, &c, dir);
            if (isLooping(r, c, dir, grid)) {
                res += 1;
            }
            _ = moveForward(&r, &c, dir);
            grid[r][c] = .visited;
        } else if (grid[r][c] == .blocked) {
            moveBackward(&r, &c, dir);
            dir = dir.rotate();
        }
    }
    return res;
}

fn isLooping(rr: usize, cc: usize, dir: Dir, g: [130][130]Type) bool {
    var r = rr;
    var c = cc;
    var d = dir;
    var grid = g;
    while (moveForward(&r, &c, d)) {
        switch (grid[r][c]) {
            .bounced => |*val| {
                if (val.* & d.toBit() != 0) {
                    return true;
                }
                val.* |= d.toBit();
                moveBackward(&r, &c, d);
                d = d.rotate();
            },
            .blocked => {
                grid[r][c] = .{ .bounced = d.toBit() };
                moveBackward(&r, &c, d);
                d = d.rotate();
            },
            else => {},
        }
    }
    return false;
}

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

const data_file = @embedFile("./data.txt");
const data: []const u8 = @as([]const u8, data_file);

pub fn main() !void {
    var n_cols: i32 = 0;
    var guard_idx: usize = 0;
    for (data, 0..) |v, idx| {
        if (n_cols == 0 and v == '\n') {
            // get the matrix dimmensions
            n_cols = @intCast(idx);
        }
        if (guard_idx == 0 and v == '^') {
            guard_idx = idx;
            break;
        }
    }

    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    var map = try allocator.alloc(u8, data.len);
    @memcpy(map, data);

    std.debug.print("{d}\n", .{n_cols});
    // directions = N, E, S, W
    const dirs = [_]i32{ -(n_cols + 1), 1, (n_cols + 1), -1 };
    var dir_idx: usize = 0; // N
    var cur_dir = dirs[dir_idx];
    while (true) {
        map[guard_idx] = 'X';
        const next_pos = move(guard_idx, cur_dir, map.len) orelse break;
        if (map[next_pos] == '\n') break; // out of map
        if (map[next_pos] == '#') {
            dir_idx = (dir_idx + 1) % 4;
            cur_dir = dirs[dir_idx];
            continue;
        }
        guard_idx = next_pos;
    }

    var countX: u32 = 0;
    for (map) |v| {
        if (v == 'X') countX += 1;
    }

    std.debug.print("{d}\n", .{countX});
}

pub fn move(pos: usize, offset: i32, max_val: usize) ?usize {
    const spos: i64 = @intCast(pos);
    const next_pos = spos + offset;
    // null value => guard out of map
    return if (next_pos < 0 or next_pos >= max_val) null else @as(usize, @intCast(next_pos));
}

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

const data_file = @embedFile("./data.txt");
const data: []const u8 = @as([]const u8, data_file);

pub fn main() !void {
    var n_cols: i32 = 0;
    var init_pos: usize = 0;
    for (data, 0..) |v, idx| {
        if (n_cols == 0 and v == '\n') {
            // get the matrix dimmensions
            n_cols = @intCast(idx);
        }
        if (v == '^') {
            init_pos = idx;
            break;
        }
    }

    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    var map = try allocator.alloc(u8, data.len);
    @memcpy(map, data);

    var visited_points = std.AutoHashMap(usize, void).init(allocator);

    // directions = N, E, S, W
    const dirs = [_]i32{ -(n_cols + 1), 1, (n_cols + 1), -1 };
    var dir_idx: usize = 0; // N
    var cur_dir = dirs[dir_idx];
    var guard_idx = init_pos;
    while (true) {
        map[guard_idx] = 'X';
        const next_pos = move(guard_idx, cur_dir, map.len) orelse break;
        if (map[next_pos] == '\n') break; // out of map
        if (map[next_pos] == '#') {
            dir_idx = (dir_idx + 1) % 4;
            cur_dir = dirs[dir_idx];
            continue;
        }
        try visited_points.put(next_pos, {});
        guard_idx = next_pos;
    }

    var p2: u32 = 0;
    var it = visited_points.keyIterator();
    while (it.next()) |point_ptr| {
        var map_modified = try allocator.alloc(u8, data.len);
        @memcpy(map_modified, data);
        map_modified[point_ptr.*] = '#';
        dir_idx = 0; // N
        cur_dir = dirs[dir_idx];
        guard_idx = init_pos;
        var visited_dir = [_]usize{1} ** data.len;
        while (true) {
            const dirp = dir2prime(dir_idx);
            if (visited_dir[guard_idx] % dirp == 0) {
                p2 += 1;
                break;
            }
            visited_dir[guard_idx] *= dirp;

            const next_pos = move(guard_idx, cur_dir, map_modified.len) orelse break;
            if (map_modified[next_pos] == '\n') break; // out of map
            if (map_modified[next_pos] == '#') {
                dir_idx = (dir_idx + 1) % 4;
                cur_dir = dirs[dir_idx];
                continue;
            }
            guard_idx = next_pos;
        }
    }

    var countX: u32 = 0;
    for (map) |v| {
        if (v == 'X') countX += 1;
    }

    std.debug.print("{d}\n", .{p2});
}

pub fn move(pos: usize, offset: i32, max_val: usize) ?usize {
    const spos: i64 = @intCast(pos);
    const next_pos = spos + offset;
    // null value => guard out of map
    return if (next_pos < 0 or next_pos >= max_val) null else @as(usize, @intCast(next_pos));
}

pub fn dir2prime(dir_idx: usize) usize {
    switch (dir_idx) {
        0 => return 2,
        1 => return 3,
        2 => return 5,
        else => return 7,
    }
}

Part 2 of today really really killed me. I kept getting ~100 extra positions. I ended up running @Travis code to see what the asnwer should be. After looking through the code and comparing I realized my mistake. My goal had been to make the checks at each step as i was discovering the answer to question 1.

Doing that, however, i forgot to exclude backtracks (times when the normal path would go back or cross itself, as you couldn’t have placed an obstacle on the route and proceeded through it.

Once I got that done, I was able to get the question don in 25 ms.

https://zigbin.io/481cc4

My run length encoding thing is a bit disgusting but I got it down to 13ms…
https://zigbin.io/6595cb

1 Like

Like others it seems, I managed to get it, but it’s so inefficient. Just curious if others have ideas: