const std = @import("std"); const aoc = @import("./aoc.zig"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const Point = @import("./point.zig").Point(u32); const Direction = enum { Up, Down, Left, Right, fn opposite(self: Direction) Direction { return switch (self) { .Up => .Down, .Down => .Up, .Left => .Right, .Right => .Left, }; } }; const Pipe = [2]Direction; const VerticalPipe = Pipe{ .Up , .Down }; const HorizontalPipe = Pipe{ .Left , .Right }; const LeftUpPipe = Pipe{ .Left , .Up }; const LeftDownPipe = Pipe{ .Left , .Down }; const RightUpPipe = Pipe{ .Right, .Up }; const RightDownPipe = Pipe{ .Right, .Down }; const pipe_variants = .{ .{ '|', VerticalPipe }, .{ '-', HorizontalPipe }, .{ 'J', LeftUpPipe }, .{ '7', LeftDownPipe }, .{ 'L', RightUpPipe }, .{ 'F', RightDownPipe }, }; const max_pipe_variants = pipe_variants.len; const TileType = enum { Ground, Start, Pipe }; const Tile = union(TileType) { Ground: void, Start: void, Pipe: Pipe, fn to_char(self: Tile) u8 { return switch (self) { .Start => 'S', .Ground => '.', .Pipe => |dirs| find_pipe_symbol(dirs[0], dirs[1]) orelse unreachable, }; } }; const Grid = struct { allocator: Allocator, width: u32, height: u32, tiles: []Tile, fn deinit(self: Grid) void { self.allocator.free(self.tiles); } fn print(self: *Grid) void { for (0..self.height) |y| { for (0..self.width) |x| { const char = self.get(@intCast(x), @intCast(y)).to_char(); const symbol = switch(char) { '.' => " ", 'S' => "S", '|' => "│", '-' => "─", 'J' => "┘", '7' => "┐", 'L' => "└", 'F' => "┌", else => unreachable }; std.debug.print("{s}", .{symbol}); } std.debug.print("\n", .{}); } } fn get_idx(self: *const Grid, x: u32, y: u32) usize { return y * self.width + x; } fn get(self: *const Grid, x: u32, y: u32) Tile { return self.tiles[self.get_idx(x, y)]; } fn set(self: *Grid, x: u32, y: u32, tile: Tile) void { self.tiles[self.get_idx(x, y)] = tile; } fn in_bounds(self: *const Grid, x: i32, y: i32) bool { return x >= 0 and y >= 0 and x < self.width and y < self.height; } }; fn does_pipe_have_direction(pipe: Pipe, dir: Direction) bool { return pipe[0] == dir or pipe[1] == dir; } fn find_pipe_variant(dir1: Direction, dir2: Direction) ?Pipe { inline for (pipe_variants) |pipe_variant| { const variant_dir1 = pipe_variant[1][0]; const variant_dir2 = pipe_variant[1][1]; if ((variant_dir1 == dir1 and variant_dir2 == dir2) or (variant_dir1 == dir2 and variant_dir2 == dir1)) { return pipe_variant[1]; } } return null; } fn find_pipe_symbol(dir1: Direction, dir2: Direction) ?u8 { inline for (pipe_variants) |pipe_variant| { const variant_dir1 = pipe_variant[1][0]; const variant_dir2 = pipe_variant[1][1]; if ((variant_dir1 == dir1 and variant_dir2 == dir2) or (variant_dir1 == dir2 and variant_dir2 == dir1)) { return pipe_variant[0]; } } return null; } fn parse_tile(char: u8) !Tile { const void_value = @as(void, undefined); if (char == 'S') { return Tile{ .Start = void_value }; } else if (char == '.') { return Tile{ .Ground = void_value }; } inline for (pipe_variants) |pipe_variant| { if (pipe_variant[0] == char) { return Tile { .Pipe = pipe_variant[1] }; } } return error.UnknownTile; } fn parse_input(allocator: Allocator, lines: []const []const u8) !Grid { const height: u32 = @intCast(lines.len); const width: u32 = @intCast(lines[0].len); var tiles = try allocator.alloc(Tile, width * height); errdefer allocator.free(tiles); for (0.., lines) |y, line| { for (0.., line) |x, char| { const idx = y * width + x; tiles[idx] = try parse_tile(char); } } return Grid{ .allocator = allocator, .width = width, .height = height, .tiles = tiles }; } fn find_starting_position(grid: *const Grid) ?Point { for (0..grid.height) |y| { for (0..grid.width) |x| { const tile = grid.get(@intCast(x), @intCast(y)); if (@as(TileType, tile) == TileType.Start) { return Point{ .x = @intCast(x), .y = @intCast(y) }; } } } return null; } fn get_neighbours(grid: *const Grid, x: u32, y: u32) std.BoundedArray(Point, 4) { const offsets = .{ .{ 0, -1 }, .{ 0, 1 }, .{ -1, 0 }, .{ 1, 0 }, }; var neighbours = std.BoundedArray(Point, 4).init(0) catch unreachable; inline for (offsets) |offset| { const ox = offset[0]; const oy = offset[1]; const nx: i32 = @intCast(@as(i32, @intCast(x)) + ox ); const ny: i32 = @intCast(@as(i32, @intCast(y)) + oy ); if (grid.in_bounds(nx, ny)) { neighbours.append(Point{ .x = @intCast(nx), .y = @intCast(ny) }) catch unreachable; } } return neighbours; } fn get_pipe_neighbours(grid: *const Grid, x: u32, y: u32) std.BoundedArray(Point, 4) { var pipe_neighbours = std.BoundedArray(Point, 4).init(0) catch unreachable; const neighbours = get_neighbours(grid, x, y); const current_tile = grid.get(x, y); assert(@as(TileType, current_tile) == TileType.Pipe); const current_pipe = current_tile.Pipe; for (neighbours.slice()) |neighbour| { const tile = grid.get(neighbour.x, neighbour.y); if (@as(TileType, tile) != TileType.Pipe) continue; var direction: Direction = undefined; if (neighbour.x > x) { direction = .Right; } else if (neighbour.x < x) { direction = .Left; } else if (neighbour.y > y) { direction = .Down; } else if (neighbour.y < y) { direction = .Up; } else { unreachable; } if (!does_pipe_have_direction(current_pipe, direction)) continue; const opposite_direction = direction.opposite(); if (does_pipe_have_direction(tile.Pipe, opposite_direction)) { pipe_neighbours.append(neighbour) catch unreachable; } } return pipe_neighbours; } fn get_connections(grid: *const Grid, x: u32, y: u32) std.BoundedArray(Direction, 4) { const directions = .{ .{ Direction.Up , 0, -1 }, .{ Direction.Down , 0, 1 }, .{ Direction.Left , -1, 0 }, .{ Direction.Right, 1, 0 }, }; var connections = std.BoundedArray(Direction, 4).init(0) catch unreachable; inline for (directions) |direction_tuple| { const direction: Direction = direction_tuple[0]; const opposite_direction = direction.opposite(); const ox = direction_tuple[1]; const oy = direction_tuple[2]; const nx: i32 = @intCast(@as(i32, @intCast(x)) + ox ); const ny: i32 = @intCast(@as(i32, @intCast(y)) + oy ); if (grid.in_bounds(nx, ny)) { const tile = grid.get(@intCast(nx), @intCast(ny)); if (@as(TileType, tile) == TileType.Pipe and does_pipe_have_direction(tile.Pipe, opposite_direction)) { connections.append(direction) catch unreachable; } } } return connections; } fn get_next_pipe(grid: *const Grid, x: u32, y: u32, current_path: []Point) ?Point { var next_pipes = get_pipe_neighbours(grid, x, y); for (next_pipes.slice()) |point| { if (current_path.len > 1) { const first_path_point = current_path[0]; const prev_path_point = current_path[current_path.len-2]; if (prev_path_point.eql(point) or first_path_point.eql(point)) continue; } return point; } return null; } fn find_loop(allocator: Allocator, grid: *const Grid, start: Point) !std.ArrayList(Point) { var loop_path = std.ArrayList(Point).init(allocator); errdefer loop_path.deinit(); try loop_path.append(start); var current_point = start; while (true) { const next_point = get_next_pipe(grid, current_point.x, current_point.y, loop_path.items); if (next_point == null) break; current_point = next_point.?; try loop_path.append(next_point.?); } return loop_path; } pub fn part1(input: *aoc.Input) !aoc.Result { const allocator = input.allocator; var grid = try parse_input(allocator, input.lines); defer grid.deinit(); const start = find_starting_position(&grid) orelse return error.StartNotFound; const start_connections = get_connections(&grid, start.x, start.y); assert(start_connections.len == 2); const pipe_on_start = find_pipe_variant(start_connections.buffer[0], start_connections.buffer[1]) orelse unreachable; grid.set(start.x, start.y, Tile{ .Pipe = pipe_on_start }); var loop_path = try find_loop(allocator, &grid, start); defer loop_path.deinit(); const loop_length = loop_path.items.len; return .{ .uint = loop_length/2 }; } pub fn part2(input: *aoc.Input) !aoc.Result { const allocator = input.allocator; var grid = try parse_input(allocator, input.lines); defer grid.deinit(); const start = find_starting_position(&grid) orelse return error.StartNotFound; const start_connections = get_connections(&grid, start.x, start.y); assert(start_connections.len == 2); const pipe_on_start = find_pipe_variant(start_connections.buffer[0], start_connections.buffer[1]) orelse unreachable; grid.set(start.x, start.y, Tile{ .Pipe = pipe_on_start }); var loop_path = try find_loop(allocator, &grid, start); defer loop_path.deinit(); try loop_path.append(loop_path.items[0]); // Shoelace theorum + Pick's theorum // https://www.reddit.com/r/adventofcode/comments/18evyu9/comment/kcqmhwk/?context=3 var twice_area: i32 = 0; for (0..(loop_path.items.len-1)) |i| { const cur = loop_path.items[i]; const next = loop_path.items[i+1]; const y_0: i32 = @intCast(cur.y); const x_0: i32 = @intCast(cur.x); const y_1: i32 = @intCast(next.y); const x_1: i32 = @intCast(next.x); twice_area += (x_0 * y_1) - (y_0 * x_1); } const result = @abs(twice_area) / 2 - (loop_path.items.len / 2 - 1); return .{ .uint = result }; } test "part 1 example 1" { const example_input = [_][]const u8{ ".....", ".S-7.", ".|.|.", ".L-J.", ".....", }; try aoc.expectAnswerUInt(part1, 4, &example_input); } test "part 1 example 2" { const example_input = [_][]const u8{ "..F7.", ".FJ|.", "SJ.L7", "|F--J", "LJ...", }; try aoc.expectAnswerUInt(part1, 8, &example_input); } test "part 2 example 1" { const example_input = [_][]const u8{ "...........", ".S-------7.", ".|F-----7|.", ".||.....||.", ".||.....||.", ".|L-7.F-J|.", ".|..|.|..|.", ".L--J.L--J.", "...........", }; try aoc.expectAnswerUInt(part2, 4, &example_input); } test "part 2 example 2" { const example_input = [_][]const u8{ ".F----7F7F7F7F-7....", ".|F--7||||||||FJ....", ".||.FJ||||||||L7....", "FJL7L7LJLJ||LJ.L-7..", "L--J.L7...LJS7F-7L7.", "....F-J..F7FJ|L7L7L7", "....L7.F7||L7|.L7L7|", ".....|FJLJ|FJ|F7|.LJ", "....FJL-7.||.||||...", "....L---J.LJ.LJLJ...", }; try aoc.expectAnswerUInt(part2, 8, &example_input); } test "part 2 example 3" { const example_input = [_][]const u8{ "FF7FSF7F7F7F7F7F---7", "L|LJ||||||||||||F--J", "FL-7LJLJ||||||LJL-77", "F--JF--7||LJLJ7F7FJ-", "L---JF-JLJ.||-FJLJJ7", "|F|F-JF---7F7-L7L|7|", "|FFJF7L7F-JF7|JL---7", "7-L-JL7||F7|L7F-7F7|", "L.L7LFJ|||||FJL7||LJ", "L7JLJL-JLJLJL--JLJ.L", }; try aoc.expectAnswerUInt(part2, 10, &example_input); }