@main struct Day23: Puzzle { func run() { part1() part2() } func part1() { var board = Board(height: 3) board.fillHomeRow(for: .a, pieces: [.c, .c]) board.fillHomeRow(for: .b, pieces: [.b, .d]) board.fillHomeRow(for: .c, pieces: [.a, .a]) board.fillHomeRow(for: .d, pieces: [.d, .b]) board.show() print("Part 1:", find(start: board)) } func part2() { var board = Board(height: 5) board.fillHomeRow(for: .a, pieces: [.c, .d, .d, .c]) board.fillHomeRow(for: .b, pieces: [.b, .c, .b, .d]) board.fillHomeRow(for: .c, pieces: [.a, .b, .a, .a]) board.fillHomeRow(for: .d, pieces: [.d, .a, .c, .b]) board.show() print("Part 2:", find(start: board)) } func find(start: Board) -> Int { let target = Board.makeGoal(height: start.height) var current = start var totalCost = 0 var possible: [Board: Int] = [:] var visited: Set = [] while current != target { visited.insert(current) for (next, cost) in current.possibleMoves() where !visited.contains(next) { possible[next] = min(possible[next, default: .max], totalCost + cost) } (current, totalCost) = possible.min { $0.value < $1.value }! possible.removeValue(forKey: current) } return totalCost } } struct Board: Hashable { enum Piece: CaseIterable, Hashable, CustomStringConvertible { case a, b, c, d var homeColumn: Int { switch self { case .a: return 2 case .b: return 4 case .c: return 6 case .d: return 8 } } var cost: Int { switch self { case .a: return 1 case .b: return 10 case .c: return 100 case .d: return 1000 } } var sign: Character { switch self { case .a: return "A" case .b: return "B" case .c: return "C" case .d: return "D" } } var description: String { String(sign) } } enum Cell: Hashable { case piece(Piece) case empty case outside } var width = 11 var height = 3 var board: [Cell] init(height: Int) { self.height = height board = Array(repeating: .outside, count: width * height) for x in 0.. Self { var board = Self(height: height) for piece in Piece.allCases { for y in 1.. [(Piece, Int, Int)] { var result: [(Piece, Int, Int)] = [] for x in 0.. [(Board, Int)] { let pieces = movablePieces() .filter { !isHome(piece: $0.0, x: $0.1, y: $0.2) } var result: [(Board, Int)] = [] for (piece, x, y) in pieces { if let newY = freeHomeRowPosition(piece: piece), freeCorridor(from: x, to: piece.homeColumn) { result.append(move(piece: piece, from: (x, y), to: (piece.homeColumn, newY))) } for newX in openCorridorPositions(x: x) { result.append(move(piece: piece, from: (x, y), to: (newX, 0))) } } return result } func freeCorridor(from x0: Int, to x1: Int) -> Bool { let minX: Int let maxX: Int if x0 < x1 { (minX, maxX) = (x0 + 1, x1) } else { (minX, maxX) = (x1, x0 - 1) } for x in minX...maxX { if self[x, 0] != .empty { return false } } return true } func move(piece: Piece, from: (Int, Int), to: (Int, Int)) -> (Board, Int) { let (x, y) = from let (newX, newY) = to precondition(self[newX,newY] == .empty) let cost = (y + distance((x, 0), (newX, newY))) * piece.cost var board = self board[x, y] = .empty board[newX, newY] = .piece(piece) return (board, cost) } func freeHomeRowPosition(piece: Piece) -> Int? { let x = piece.homeColumn var depth = height - 1 loop: while depth > 0 { switch self[x, depth] { case .piece(piece): break case .piece: return nil case .empty: break loop case .outside: preconditionFailure("Invalid board") } depth -= 1 } return depth } static let homeColumns = Set(Piece.allCases.map(\.homeColumn)) func openCorridorPositions(x: Int) -> [Int] { var xmin = x var xmax = x while xmin > 0 && self[xmin, 0] == .empty { xmin -= 1 } while xmax < width && self[xmax, 0] == .empty { xmax += 1 } let result = (xmin.. Int { let (x0, y0) = a let (x1, y1) = b return abs(x1 - x0) + abs(y1 - y0) } func isHome(piece: Piece, x: Int, y: Int) -> Bool { guard x == piece.homeColumn && y > 0 else { return false } for otherY in (y + 1).. Cell { get { board[x + width * y] } set { board[x + width * y] = newValue } } }