//! This test solves first part of https://adventofcode.com/2021/day/23 const std = @import("std"); const math = std.math; const Str = []const u8; const Amphipod = enum { A, B, C, D, }; const HALLWAY_LENGTH = 7; const SIDEROOMS = 4; const SIDEROOM_DEPTH = 2; const PathType = u4; const State = struct { hallway: [HALLWAY_LENGTH]?Amphipod = .{null} ** HALLWAY_LENGTH, rooms: [SIDEROOMS][SIDEROOM_DEPTH]?Amphipod, pub fn print(self: @This()) []u8 { const blank = \\############# \\#...........# \\###.#.#.#.### \\ #.#.#.#.# \\ ######### ; var buffer: [100]u8 = undefined; var mod: []u8 = buffer[0..blank.len]; std.mem.copy(u8, mod, blank); for (self.hallway, 0..) |amp, idx| { if (amp != null) { const i: usize = switch (idx) { 0 => 14 + 1, 1 => 14 + 2, 2 => 14 + 4, 3 => 14 + 6, 4 => 14 + 8, 5 => 14 + 10, 6 => 14 + 11, else => unreachable, }; const a: u8 = switch (amp.?) { .A => 'A', .B => 'B', .C => 'C', .D => 'D', }; mod[i] = a; } } for (self.rooms, 0..) |room, rid| { for (room, 0..) |amp, aid| { if (amp != null) { const a: u8 = switch (amp.?) { .A => 'A', .B => 'B', .C => 'C', .D => 'D', }; mod[14 + 14 + rid * 2 + 3 + 14 * aid] = a; } } } return mod; } fn sideRoomDone(self: @This(), room: usize) bool { std.debug.assert(room < SIDEROOMS); for (self.rooms[room]) |amp| { if (amp == null or amp != @as(Amphipod, @enumFromInt(room))) return false; } return true; } fn sideRoomFreeSpace(self: @This(), room: usize) ?u2 { var ret: u2 = 0; std.debug.assert(room < SIDEROOMS); for (self.rooms[room]) |amp| { if (amp == null) { ret += 1; } else if (amp != @as(Amphipod, @enumFromInt(room))) { return null; } } std.debug.assert(ret <= SIDEROOM_DEPTH); return ret; } fn checkSideRoom(self: @This()) ?StateQueue { for (self.rooms, 0..) |_, idx| { const free = self.sideRoomFreeSpace(idx) orelse continue; if (free == 0) continue; // std.debug.print("FREE: {d}\n", .{free}); for (self.hallway, 0..) |amp, hwidx| { if (amp == @as(Amphipod, @enumFromInt(idx))) { const steps = (self.checkHWtoRoom(hwidx, idx) orelse continue) + free; var ret: StateQueue = undefined; ret.cost = (std.math.powi(usize, 10, @intFromEnum(amp.?)) catch unreachable) * steps; ret.state = self; ret.state.hallway[hwidx] = null; ret.state.rooms[idx][free - 1] = amp; return ret; } } for (self.rooms, 0..) |other, oidx| { if (idx == oidx) continue; var pos: PathType = 0; for (other) |amp| { if (amp == null) { pos += 1; continue; } if (amp == @as(Amphipod, @enumFromInt(idx))) { var steps: PathType = undefined; if (idx < oidx) { // moving left const hwidx = oidx + 2; // std.debug.print("L->R from: {d} to: {d}\n", .{ hwidx, idx }); steps = (self.checkHWtoRoom(hwidx, idx) orelse break) + free + pos; } else { // moving right const hwidx = oidx + 1; // std.debug.print("R->L from: {d} to: {d}\n", .{ hwidx, idx }); steps = (self.checkHWtoRoom(hwidx, idx) orelse break) + free + pos; } // we can move amp to his sideroom var ret: StateQueue = undefined; ret.cost = (std.math.powi(usize, 10, @intFromEnum(amp.?)) catch unreachable) * steps; ret.state = self; ret.state.rooms[oidx][pos] = null; ret.state.rooms[idx][free - 1] = amp; return ret; } // only first item can move, no need to check others break; } } } return null; } fn checkHWtoRoom(self: @This(), from: usize, to: usize) ?PathType { // ############# // #01.2.3.4.56# // ###0#1#2#3### // #0#1#2#3# // ######### std.debug.assert(to < 4); std.debug.assert(from < 8); var steps: PathType = 0; if (from -| to >= 2) { var idx: usize = from -| 1; while (idx >= to + 2) : (idx -= 1) { // std.debug.print("LEFT from: {d} to: {d} step: {d}\n", .{ from, to, idx }); if (self.hallway[idx] == null) steps += 2 else return null; } } else { var idx: usize = from + 1; while (idx <= to + 1) : (idx += 1) { // std.debug.print("RIGHT from: {d} to: {d} step: {d}\n", .{ from, to, idx }); if (self.hallway[idx] == null) steps += 2 else return null; } } // room entrance point if (from != 6 and from != 0) steps += 1; return steps; } pub fn neighborFn(self: @This(), allocator: std.mem.Allocator) ![]StateQueue { var ret = std.ArrayList(StateQueue).init(allocator); // Amphipod can move to its final place, always a good move if (self.checkSideRoom()) |item| { try ret.append(item); return ret.toOwnedSlice(); } // we already checked the side rooms, so we only have to collect // each rooms first item's possible hallway positions for (self.rooms, 0..) |room, idx| { // std.debug.print("ROOM->HW check {d} room\n", .{idx}); for (room, 0..) |amp, pos| { if (amp == null) continue; // do not move items in right room // except: we should move away to let wrong placed amp out if (amp == @as(Amphipod, @enumFromInt(idx))) { var tainted: bool = false; var next: PathType = @as(PathType, @intCast(pos)) + 1; while (next < SIDEROOM_DEPTH) : (next += 1) { if (room[next] != @as(Amphipod, @enumFromInt(idx))) tainted = true; } if (!tainted) break; } for (self.hallway, 0..) |hw, hwidx| { if (hw == null) { // destination hallway spot is free // std.debug.print("checking hallway spot {d} with amp {any}\n", .{hwidx, amp}); const steps = (self.checkHWtoRoom(hwidx, idx) orelse continue) + pos + 1; var st: StateQueue = undefined; st.cost = steps * try std.math.powi(usize, 10, @intFromEnum(amp.?)); st.state = self; st.state.hallway[hwidx] = amp; st.state.rooms[idx][pos] = null; try ret.append(st); } } // only first item can move break; } } return ret.toOwnedSlice(); } }; fn parseInput(comptime input: Str) !State { var lines = std.mem.split(u8, input, "\n"); var ret = State{ .rooms = undefined }; var line: usize = 0; var counter: usize = 0; while (lines.next()) |l| : (line += 1) { if (line == 2 or line == 3) { for (l, 0..) |ch, idx| { if (idx == 3 or idx == 5 or idx == 7 or idx == 9) { const amp: Amphipod = switch (ch) { 'A' => .A, 'B' => .B, 'C' => .C, 'D' => .D, else => unreachable, }; // std.debug.print("{d} {d}\n", .{counter%siderooms, counter/siderooms}); ret.rooms[counter % SIDEROOMS][counter / SIDEROOMS] = amp; counter += 1; } } } } return ret; } const PF = @import("pathfinder.zig"); const StateQueue = PF.Graph.Graph(State).QueueType; test "Graph" { const start_str = \\############# \\#...........# \\###D#D#B#A### \\ #B#C#A#C# \\ ######### ; const goal_str = \\############# \\#...........# \\###A#B#C#D### \\ #A#B#C#D# \\ ######### ; const start = try parseInput(start_str); const goal = try parseInput(goal_str); var graph = PF.Graph.Graph(State).init(std.testing.allocator); defer graph.deinit(); const ret = try graph.dijkstra(start, goal); try std.testing.expectEqual(@as(usize, 16244), ret); }