426 lines
12 KiB
Zig
426 lines
12 KiB
Zig
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);
|
|
}
|