diff --git a/src/day23.zig b/src/day23.zig new file mode 100644 index 0000000..6ad3afd --- /dev/null +++ b/src/day23.zig @@ -0,0 +1,432 @@ +const aoc = @import("./aoc.zig"); +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const PointU32 = @import("./point.zig").PointU32; +const PointI32 = @import("./point.zig").PointI32; + +const neighbours = .{ + .{ 1, 0 }, + .{ -1, 0 }, + .{ 0, 1 }, + .{ 0, -1 }, +}; + +const Tile = enum { + Empty, + Wall, + DownSlope, + UpSlope, + RightSlope, + LeftSlope, + + fn fromU8(char: u8) ?Tile { + return switch(char) { + '.' => .Empty, + '#' => .Wall, + 'v' => .DownSlope, + '>' => .RightSlope, + '<' => .LeftSlope, + '^' => .UpSlope, + else => null + }; + } + + fn getSlopeDirection(self: Tile) ?PointI32 { + return switch (self) { + .DownSlope => PointI32{ .x = 0, .y = 1 }, + .UpSlope => PointI32{ .x = 0, .y = -1 }, + .LeftSlope => PointI32{ .x = -1, .y = 0 }, + .RightSlope => PointI32{ .x = 1, .y = 0 }, + else => null + }; + } +}; + +const Map = struct { + allocator: Allocator, + tiles: []Tile, + width: u32, + height: u32, + + fn init(allocator: Allocator, width: u32, height: u32) !Map { + return Map{ + .tiles = try allocator.alloc(Tile, width * height), + .allocator = allocator, + .width = width, + .height = height + }; + } + + fn get(self: Map, x: u32, y: u32) Tile { + return self.tiles[self.getIndex(x, y)]; + } + + fn getIndex(self: Map, x: u32, y: u32) usize { + assert(0 <= x and x < self.width); + assert(0 <= y and y < self.height); + return y * self.width + x; + } + + fn deinit(self: Map) void { + self.allocator.free(self.tiles); + } +}; + +fn parseInput(allocator: Allocator, lines: []const []const u8) !Map { + const width = lines[0].len; + const height = lines.len; + const map = try Map.init(allocator, @intCast(width), @intCast(height)); + errdefer map.deinit(); + + for (0.., lines) |y, line| { + for (0.., line) |x, char| { + const idx = y * width + x; + map.tiles[idx] = Tile.fromU8(char) orelse return error.InvalidTile; + } + } + + return map; +} + +fn findEmptySpot(map: Map, y: u32) ?u32 { + for (0..map.width) |x| { + const idx = y * map.width + x; + if (map.tiles[idx] == .Empty) { + return @intCast(x); + } + } + return null; +} + +const NodeRegionId = u8; +const RegionConnectionsArray = std.BoundedArray(NodeRegionId, 4); +const NodeRegion = struct { + size: u32, + connections: RegionConnectionsArray +}; + +const NodeMap = struct { + allocator: Allocator, + regions: []NodeRegion, + region_map: []?NodeRegionId, + width: u32, + height: u32, + + fn init(allocator: Allocator, map: Map) !NodeMap { + const region_map = try allocator.alloc(?NodeRegionId, map.width * map.height); + errdefer allocator.free(region_map); + @memset(region_map, null); + + var region_count: NodeRegionId = 0; + while (findUnmarkedPoint(map, region_map)) |point| { + try floodFillRegion(allocator, point, region_count, map, region_map); + + region_count += 1; + } + + var regions = try allocator.alloc(NodeRegion, region_count); + errdefer allocator.free(regions); + + for (regions) |*region| { + region.size = 0; + region.connections = RegionConnectionsArray.init(0) catch unreachable; + } + + for (0..map.height) |y| { + for (0..map.width) |x| { + const point = PointU32{ + .x = @intCast(x), + .y = @intCast(y) + }; + + const slope_dir = map.get(point.x, point.y).getSlopeDirection(); + if (slope_dir == null) continue; + + const from_point = point.toI32().sub(slope_dir.?).toU32(); + const to_point = point.toI32().add(slope_dir.?).toU32(); + + const from_region = region_map[map.getIndex(from_point.x, from_point.y)].?; + const to_region = region_map[map.getIndex(to_point.x, to_point.y)].?; + + try regions[from_region].connections.append(to_region); + + if (isIntersection(map, to_point.x, to_point.y)) { + region_map[map.getIndex(point.x, point.y)] = from_region; + } else { + region_map[map.getIndex(point.x, point.y)] = to_region; + } + } + } + + for (region_map) |maybe_region_id| { + if (maybe_region_id) |region_id| { + regions[region_id].size += 1; + } + } + + return NodeMap{ + .allocator = allocator, + .regions = regions, + .region_map = region_map, + .width = map.width, + .height = map.height, + }; + } + + fn deinit(self: NodeMap) void { + self.allocator.free(self.regions); + self.allocator.free(self.region_map); + } + + fn findUnmarkedPoint(map: Map, region_map: []const ?NodeRegionId) ?PointU32 { + for (0..map.height) |y| { + for (0..map.width) |x| { + const idx = y * map.width + x; + if (region_map[idx] == null and map.tiles[idx] == .Empty) { + return PointU32{ + .x = @intCast(x), + .y = @intCast(y), + }; + } + } + } + return null; + } + + fn floodFillRegion( + allocator: Allocator, + from: PointU32, + region_id: NodeRegionId, + map: Map, + region_map: []?NodeRegionId + ) !void { + var stack = std.ArrayList(PointU32).init(allocator); + defer stack.deinit(); + + try stack.append(from); + region_map[map.getIndex(from.x, from.y)] = region_id; + + while (stack.popOrNull()) |point| { + if (map.get(point.x, point.y) != .Empty) continue; + + inline for (neighbours) |neighbour| { + const nx: i32 = @as(i32, @intCast(point.x)) + neighbour[0]; + const ny: i32 = @as(i32, @intCast(point.y)) + neighbour[1]; + if ((0 <= nx and nx < map.width) and (0 <= ny and ny < map.height)) { + const next_point = PointU32{ + .x = @intCast(nx), + .y = @intCast(ny) + }; + const next_index = map.getIndex(next_point.x, next_point.y); + if (map.get(next_point.x, next_point.y) == .Empty and region_map[next_index] == null) { + try stack.append(next_point); + region_map[next_index] = region_id; + } + } + } + } + } + + fn isIntersection(map: Map, x: u32, y: u32) bool { + inline for (neighbours) |neighbour| { + const nx = @as(i32, @intCast(x)) + neighbour[0]; + const ny = @as(i32, @intCast(y)) + neighbour[1]; + if (map.get(@intCast(nx), @intCast(ny)) == .Empty) { + return false; + } + } + return true; + } + + fn debug(self: NodeMap) void { + for (0..self.height) |y| { + for (0..self.width) |x| { + const idx = @as(usize, @intCast(self.width)) * y + x; + if (self.region_map[idx]) |region_id| { + std.debug.print("{c}", .{ region_id + 'A' }); + } else { + std.debug.print(".", .{}); + } + } + std.debug.print("\n", .{}); + } + + for (0.., self.regions) |region_id, region| { + std.debug.print("Region {c} ({}):", .{@as(u8, @intCast(region_id)) + 'A', region.size}); + for (region.connections.constSlice()) |other_region_id| { + std.debug.print(" {c}", .{@as(u8, @intCast(other_region_id)) + 'A'}); + } + std.debug.print("\n", .{}); + } + } +}; + +const VisitedRegionBitSet = std.bit_set.ArrayBitSet(usize, 128); +const QueueItem = struct { + region: NodeRegionId, + distance: u32, + visited: VisitedRegionBitSet, +}; + +const WalkPriorityQueue = std.PriorityQueue(QueueItem, void, compareQueueItem); + +fn compareQueueItem(_: void, a: QueueItem, b: QueueItem) std.math.Order { + return std.math.order(b.distance, a.distance); +} + +fn find_longest_path(allocator: Allocator, node_map: NodeMap, start: PointU32, end: PointU32, max_best_distances: ?u32) !u64 { + const start_region = node_map.region_map[node_map.width * start.y + start.x].?; + const end_region = node_map.region_map[node_map.width * end.y + end.x].?; + + const SeenEntry = struct { + visited: VisitedRegionBitSet, + }; + var seen = std.AutoHashMap(SeenEntry, void).init(allocator); + defer seen.deinit(); + + var queue = WalkPriorityQueue.init(allocator, {}); + defer queue.deinit(); + + { + var visited = VisitedRegionBitSet.initEmpty(); + visited.set(start_region); + try queue.add(QueueItem{ + .distance = node_map.regions[start_region].size - 1, + .region = start_region, + .visited = visited + }); + } + + var best_distance: u32 = 0; + var best_distance_count: u32 = 0; + + while (queue.removeOrNull()) |item| { + const region_id = item.region; + if (region_id == end_region) { + if (best_distance > item.distance) { + best_distance_count += 1; + + // TODO: A hack to make this algorithm finish in a reasonable amount of time for part 2, + // The idea is that if the best distance does not change for a while, it is probably the best. + // Probably... + // This assumption should mostly work and not give false positives, because a PriorityQueue was used. + if (best_distance_count == max_best_distances) break; + } else { + best_distance_count = 0; + } + + best_distance = @max(best_distance, item.distance); + continue; + } + + const region = node_map.regions[region_id]; + for (region.connections.constSlice()) |other_region_id| { + const other_region = node_map.regions[other_region_id]; + if (item.visited.isSet(other_region_id)) continue; + + var visited = item.visited; + visited.set(other_region_id); + const seen_entry = SeenEntry{ + .visited = visited, + }; + if (seen.contains(seen_entry)) continue; + + try seen.put(seen_entry, {}); + try queue.add(QueueItem{ + .distance = item.distance + other_region.size, + .region = other_region_id, + .visited = visited + }); + } + } + + return best_distance; +} + +pub fn part1(input: *aoc.Input) !aoc.Result { + const allocator = input.allocator; + const map = try parseInput(allocator, input.lines); + defer map.deinit(); + + const node_map = try NodeMap.init(allocator, map); + defer node_map.deinit(); + + const start = PointU32{ + .x = findEmptySpot(map, 0) orelse return error.NoStart, + .y = 0 + }; + const end = PointU32{ + .x = findEmptySpot(map, map.height-1) orelse return error.NoEnd, + .y = map.height-1 + }; + + const answer = try find_longest_path(allocator, node_map, start, end, null); + + return .{ .uint = answer }; +} + +pub fn part2(input: *aoc.Input) !aoc.Result { + const allocator = input.allocator; + const map = try parseInput(allocator, input.lines); + defer map.deinit(); + + const node_map = try NodeMap.init(allocator, map); + defer node_map.deinit(); + + for (0.., node_map.regions) |region_id, *region| { + for (region.connections.constSlice()) |other_region_id| { + const other_region = &node_map.regions[other_region_id]; + if (std.mem.indexOfScalar(NodeRegionId, other_region.connections.constSlice(), @intCast(region_id)) == null) { + try other_region.connections.append(@intCast(region_id)); + } + } + } + + const start = PointU32{ + .x = findEmptySpot(map, 0) orelse return error.NoStart, + .y = 0 + }; + const end = PointU32{ + .x = findEmptySpot(map, map.height-1) orelse return error.NoEnd, + .y = map.height-1 + }; + + const answer = try find_longest_path(allocator, node_map, end, start, 1000); + + return .{ .uint = answer }; +} + +const example_input = [_][]const u8{ + "#.#####################", + "#.......#########...###", + "#######.#########.#.###", + "###.....#.>.>.###.#.###", + "###v#####.#v#.###.#.###", + "###.>...#.#.#.....#...#", + "###v###.#.#.#########.#", + "###...#.#.#.......#...#", + "#####.#.#.#######.#.###", + "#.....#.#.#.......#...#", + "#.#####.#.#.#########v#", + "#.#...#...#...###...>.#", + "#.#.#v#######v###.###v#", + "#...#.>.#...>.>.#.###.#", + "#####v#.#.###v#.#.###.#", + "#.....#...#...#.#.#...#", + "#.#########.###.#.#.###", + "#...###...#...#...#.###", + "###.###.#.###v#####v###", + "#...#...#.#.>.>.#.>.###", + "#.###.###.#.###.#.#v###", + "#.....###...###...#...#", + "#####################.#", +}; + +test "part 1 example" { + try aoc.expectAnswerUInt(part1, 94, &example_input); +} + +test "part 2 example" { + try aoc.expectAnswerUInt(part2, 154, &example_input); +} diff --git a/src/main.zig b/src/main.zig index aafc478..9781451 100644 --- a/src/main.zig +++ b/src/main.zig @@ -43,6 +43,7 @@ const Days = [_]aoc.Day{ create_day(@import("./day20.zig")), create_day(@import("./day21.zig")), create_day(@import("./day22.zig")), + create_day(@import("./day23.zig")), }; fn kilobytes(count: u32) u32 { @@ -208,4 +209,5 @@ test { _ = @import("./day20.zig"); _ = @import("./day21.zig"); _ = @import("./day22.zig"); + _ = @import("./day23.zig"); }