const std = @import("std"); fn Order(comptime T: type) type { return struct { fn order(context: void, lhs: T, rhs: T) std.math.Order { _ = context; return std.math.order(lhs, rhs); } }; } fn StackList(comptime T: type, comptime capacity_type: type, comptime capacity: capacity_type) type { return struct { const Self = @This(); mem: [capacity]T, length: capacity_type, fn add(self: *Self, value: T) void { self.mem[self.length] = value; self.length += 1; } fn addIfNotNull(self: *Self, nullable_value: ?T) void { if (nullable_value) |value| { self.add(value); } } fn has(self: *const Self, needle: T) bool { for (0..self.length) |i| { if (self.mem[i] == needle) { return true; } } return false; } fn addUnique(self: *Self, value: T) void { if (!self.has(value)) { self.add(value); } } fn getMutableSlice(self: *Self) []T { return (&self.mem)[0..self.length]; } fn getSlice(self: *const Self) []const T { return self.mem[0..self.length]; } fn init() Self { return Self{ .mem = undefined, .length = 0, }; } }; } fn RingQueueWrapper(comptime T: type, comptime size: usize) type { return struct { const Self = @This(); mem: *[size]T, first: usize, next: usize, fn add(self: *Self, value: T) void { std.debug.assert(self.next - self.first < size); self.mem.*[self.next % size] = value; self.next += 1; } fn take(self: *Self) T { const result = self.mem.*[self.first % size]; self.first += 1; return result; } fn isEmpty(self: *const Self) bool { return self.next == self.first; } fn init(mem: *[size]T) Self { return Self{ .mem = mem, .first = 0, .next = 0, }; } }; } fn readNumber(comptime T: type, line: []const u8, index: *usize) T { var result: T = 0; while (index.* < line.len and line[index.*] == ' ') : (index.* += 1) {} var is_negative = false; if (index.* < line.len and line[index.*] == '-') { is_negative = true; index.* += 1; } std.debug.assert(index.* < line.len); while (index.* < line.len) : (index.* += 1) { const char = line[index.*]; switch (char) { '0'...'9' => { result = result * 10 + (char - '0'); }, else => { break; }, } } return if (is_negative) (0 - result) else result; } fn readHexNumber(comptime T: type, line: []const u8, index: *usize) T { var result: T = 0; while (index.* < line.len and line[index.*] == ' ') : (index.* += 1) {} var is_negative = false; if (index.* < line.len and line[index.*] == '-') { is_negative = true; index.* += 1; } std.debug.assert(index.* < line.len); while (index.* < line.len) : (index.* += 1) { const char = line[index.*]; switch (char) { '0'...'9' => { result = result * 16 + (char - '0'); }, 'a'...'f' => { result = result * 16 + 10 + (char - 'a'); }, 'A'...'F' => { result = result * 16 + 10 + (char - 'A'); }, else => { break; }, } } return if (is_negative) (0 - result) else result; } fn Coordinates(comptime CoordinateType: type) type { return struct { const Self = @This(); x: CoordinateType, y: CoordinateType, fn isSameX(self: *const Self, other: Self) bool { return self.x == other.x; } fn approachX(self: *Self, other: Self) void { if (self.x < other.x) { self.x += 1; } else { self.x -= 1; } } fn isSameY(self: *const Self, other: Self) bool { return self.y == other.y; } fn approachY(self: *Self, other: Self) void { if (self.y < other.y) { self.y += 1; } else { self.y -= 1; } } }; } fn GenericTerrain(comptime CoordinateType: type, comptime height: CoordinateType, comptime width: CoordinateType, comptime CellType: type) type { return struct { const Self = @This(); coordinates: Coordinates(CoordinateType), cells: [height][width]CellType, fn initZeroes() Self { return .{ .coordinates = .{ .x = height / 2, .y = width / 2, }, .cells = std.mem.zeroes([height][width]CellType), }; } fn getCurrentCell(self: *const Self) CellType { return self.cells[self.coordinates.x][self.coordinates.y]; } fn setCurrentCell(self: *Self, value: CellType) void { self.cells[self.coordinates.x][self.coordinates.y] = value; } }; } const CellStatus = enum(u2) { Default = 0, NotDug = 1, Dug = 2, }; const TerrainCoordinate = u16; const Terrain = GenericTerrain(TerrainCoordinate, 2048, 2048, CellStatus); const TerrainCoordinates = Coordinates(TerrainCoordinate); const Direction = enum(u8) { Right = 0, Down = 1, Left = 2, Up = 3, }; const RawCoordinate = u32; const RawCommand = packed struct(u32) { direction: Direction, length: u24, }; const RawCoordinates = Coordinates(RawCoordinate); fn floodTerrain(terrain: *Terrain) void { var queue_mem: [65_536]TerrainCoordinates = undefined; var tasks = RingQueueWrapper(TerrainCoordinates, queue_mem.len).init(&queue_mem); tasks.add(.{ .x = 0, .y = 0 }); while (!tasks.isEmpty()) { const task = tasks.take(); terrain.coordinates = task; //std.debug.print("Processing task for {d},{d} (current status: {any})\n", .{ x, y, terrain.*.cells[x][y] }); if (terrain.getCurrentCell() == .Default) { terrain.setCurrentCell(.NotDug); //std.debug.print("Marked {d},{d} as NotDug\n", .{ x, y }); const x = task.x; const y = task.y; if (x > 0) { tasks.add(.{ .x = x - 1, .y = y }); } if (x + 1 < terrain.*.cells.len) { tasks.add(.{ .x = x + 1, .y = y }); } if (y > 0) { tasks.add(.{ .x = x, .y = y - 1 }); } if (y + 1 < terrain.*.cells[x].len) { tasks.add(.{ .x = x, .y = y + 1 }); } } } } fn getTargetRawCoordinates(coordinates: RawCoordinates, command: RawCommand) RawCoordinates { return RawCoordinates{ .x = switch (command.direction) { .Right => coordinates.x + command.length, .Left => coordinates.x - command.length, .Up, .Down => coordinates.x, }, .y = switch (command.direction) { .Down => coordinates.y + command.length, .Up => coordinates.y - command.length, .Left, .Right => coordinates.y, }, }; } fn getCoordinate(known_values: []const RawCoordinate, raw_coordinate: RawCoordinate) TerrainCoordinate { const maybe_index = std.sort.binarySearch(RawCoordinate, raw_coordinate, known_values, {}, Order(RawCoordinate).order); return @as(TerrainCoordinate, @intCast(maybe_index.?)); } fn solveForCommands(raw_commands: []const RawCommand) usize { const start_value = 1 << 23; var raw_coordinates = RawCoordinates{ .x = start_value, .y = start_value, }; var known_values = StackList(RawCoordinate, u16, 2048).init(); known_values.addUnique(0); known_values.addUnique(start_value - 1); known_values.addUnique(start_value); known_values.addUnique(start_value + 1); known_values.addUnique(std.math.maxInt(RawCoordinate)); for (raw_commands) |raw_command| { raw_coordinates = getTargetRawCoordinates(raw_coordinates, raw_command); known_values.addUnique(raw_coordinates.x - 1); known_values.addUnique(raw_coordinates.x); known_values.addUnique(raw_coordinates.x + 1); known_values.addUnique(raw_coordinates.y - 1); known_values.addUnique(raw_coordinates.y); known_values.addUnique(raw_coordinates.y + 1); } std.sort.heap(RawCoordinate, known_values.getMutableSlice(), {}, std.sort.asc(RawCoordinate)); var known_values_slice = known_values.getSlice(); raw_coordinates = RawCoordinates{ .x = start_value, .y = start_value, }; var terrain = Terrain.initZeroes(); terrain.coordinates = .{ .x = getCoordinate(known_values_slice, start_value), .y = getCoordinate(known_values_slice, start_value), }; for (raw_commands) |raw_command| { raw_coordinates = getTargetRawCoordinates(raw_coordinates, raw_command); const target = TerrainCoordinates{ .x = getCoordinate(known_values_slice, raw_coordinates.x), .y = getCoordinate(known_values_slice, raw_coordinates.y), }; while (!terrain.coordinates.isSameX(target)) { terrain.coordinates.approachX(target); terrain.setCurrentCell(.Dug); } while (!terrain.coordinates.isSameY(target)) { terrain.coordinates.approachY(target); terrain.setCurrentCell(.Dug); } } floodTerrain(&terrain); const cells = terrain.cells; var result: u64 = 0; for (0..cells.len) |x| { for (0..cells[x].len) |y| { if (cells[x][y] != .NotDug) { // Doesn't really matter whether we take x..x+1 or x-1..x here, because boundary blocks will always have size 1 result += @as(u64, known_values_slice[x + 1] - known_values_slice[x]) * @as(u64, known_values_slice[y + 1] - known_values_slice[y]); } } } return result; } fn parseCommand(command: []const u8) RawCommand { var index: usize = 2; _ = readNumber(u8, command, &index); index += 3; const value = readHexNumber(u24, command, &index); const direction: Direction = switch (value % 16) { 0 => .Right, 1 => .Down, 2 => .Left, 3 => .Up, else => unreachable, }; return .{ .length = value / 16, .direction = direction, }; } fn solveAll(reader: anytype) !usize { var result: usize = 0; while (true) { var commands = StackList(RawCommand, usize, 1024).init(); var empty_line_reached = false; var line_buffer: [1000]u8 = undefined; while (try reader.readUntilDelimiterOrEof(&line_buffer, '\n')) |line| { if (line.len == 0) { empty_line_reached = true; break; } commands.add(parseCommand(line)); } result += solveForCommands(commands.getSlice()); if (!empty_line_reached) { return result; } } } pub fn main() !void { const stdout = std.io.getStdOut().writer(); const raw_in = std.io.getStdIn(); var buffered_reader = std.io.bufferedReader(raw_in.reader()); var reader = buffered_reader.reader(); const result = try solveAll(&reader); try stdout.print("{d}\n", .{result}); }