Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Solving the 15-puzzle in Swift: A Look at Algor...

Bas Broek
August 29, 2019

Solving the 15-puzzle in Swift: A Look at Algorithms and Speed

Algorithms and optimization can sound daunting, but are a really interesting programming problem. In this talk, we'll be looking at writing a solver for a puzzle, improving it along the way by making it more performant.

Bas Broek

August 29, 2019
Tweet

More Decks by Bas Broek

Other Decks in Programming

Transcript

  1. SOLVING THE 15 PUZZLE IN SWIFT A Look at Algorithms

    and Speed A Look at Algorithms and Performance 2 — @basthomas
  2. | 1| 2| 3| 4| | 5| 6| 7| 8|

    | 9|10|11|12| |13|14|15| | 4 — @basthomas
  3. | 1| 6| 2| 3| | |10| 7| 4| |

    5| 9|11| 8| |13|14|15|12| 5 — @basthomas
  4. | 1| 6| 2| 3| | 5|10| 7| 4| |

    | 9|11| 8| |13|14|15|12| 6 — @basthomas
  5. | 1| 6| 2| 3| | 5|10| 7| 4| |

    9| |11| 8| |13|14|15|12| 7 — @basthomas
  6. | 1| 6| 2| 3| | 5| | 7| 4|

    | 9|10|11| 8| |13|14|15|12| 8 — @basthomas
  7. | 1| | 2| 3| | 5| 6| 7| 4|

    | 9|10|11| 8| |13|14|15|12| 9 — @basthomas
  8. | 1| 2| | 3| | 5| 6| 7| 4|

    | 9|10|11| 8| |13|14|15|12| 10 — @basthomas
  9. | 1| 2| 3| | | 5| 6| 7| 4|

    | 9|10|11| 8| |13|14|15|12| 11 — @basthomas
  10. | 1| 2| 3| 4| | 5| 6| 7| |

    | 9|10|11| 8| |13|14|15|12| 12 — @basthomas
  11. | 1| 2| 3| 4| | 5| 6| 7| 8|

    | 9|10|11| | |13|14|15|12| 13 — @basthomas
  12. | 1| 2| 3| 4| | 5| 6| 7| 8|

    | 9|10|11|12| |13|14|15| | 14 — @basthomas
  13. struct Solution<Problem> { struct Step<T> { let step: T }

    let steps: [Step<Problem>] var input: Step<Problem> { return steps.first! } var output: Step<Problem> { return steps.last! } init(steps: [Step<Problem>]) { precondition(steps.count > 0, "Solution must contain at least one step.") self.steps = steps } } 23 — @basthomas
  14. let solution = Solution<Int>( steps: Solution.Step(step: 1), Solution.Step(step: 2), Solution.Step(step:

    3) ) print(solution.input) // Step<Int>(step: 1) print(solution.output) // Step<Int>(step: 3) 24 — @basthomas
  15. struct Solution<Problem>: Collection { var startIndex: Int { return steps.startIndex

    } var endIndex: Int { return steps.endIndex } subscript(i: Int) -> Step<Problem> { return steps[i] } func index(after i: Int) -> Int { return steps.index(after: i) } } 26 — @basthomas
  16. struct Board { struct Position {} enum Tile { case

    empty, number(Int) } func next() func position(for tile: Tile) -> Position func tile(at position: Position) -> Tile func swap(_ aTile: Tile, with bTile: Tile) -> Bool func move(tile: Tile) -> Bool func shuffle(moves: Int = 50) func adjacentPositions(to position: Position) -> [Position] func solve() -> Solution<Board> } 30 — @basthomas
  17. struct Position: CustomStringConvertible, Equatable { let row: Int let column:

    Int func isAdjacent(to position: Position, in board: Board) -> Bool { return board.adjacentPositions(to: self) .filter { $0 == position } .isEmpty == false } var description: String { return "row: \(row), column: \(column)" } } 31 — @basthomas
  18. func isAdjacent(to position: Position, in board: Board) -> Bool {

    return board.adjacentPositions(to: self) - .filter { $0 == position } - .isEmpty == false + .first { $0 == position } != nil } 32 — @basthomas
  19. enum Tile: Equatable, ExpressibleByIntegerLiteral { case empty, number(Int) private static

    let emptyValue = -1 init(integerLiteral value: Int) { if value == Tile.emptyValue { self = .empty } else { self = .number(value) } } var intValue: Int { switch self { case .empty: return Tile.emptyValue case .number(let number): return number } } } 36 — @basthomas
  20. init(rows: Int) { precondition(rows > 1, "A puzzle should at

    least be 2x2") var _board: [[Tile]] = [] for row in 0..<rows { _board.append([]) for column in 0..<rows { let number = (row * rows) + column + 1 if row == rows - 1 && column == rows - 1 { _board[row].append(.empty) } else { _board[row].append(.number(number)) } } } } 38 — @basthomas
  21. init(rows: Int) { precondition(rows > 1, "A puzzle should at

    least be 2x2") var _board: [[Tile]] = [] for row in 0..<rows { _board.append([]) for column in 0..<rows { let number = (row * rows) + column + 1 if row == rows - 1 && column == rows - 1 { _board[row].append(.empty) } else { _board[row].append(.number(number)) } } } } 38 — @basthomas
  22. init(rows: Int) { precondition(rows > 1, "A puzzle should at

    least be 2x2") var _board: [[Tile]] = [] for row in 0..<rows { _board.append([]) for column in 0..<rows { let number = (row * rows) + column + 1 if row == rows - 1 && column == rows - 1 { _board[row].append(.empty) } else { _board[row].append(.number(number)) } } } } 38 — @basthomas
  23. init(rows: Int) { precondition(rows > 1, "A puzzle should at

    least be 2x2") var _board: [[Tile]] = [] for row in 0..<rows { _board.append([]) for column in 0..<rows { let number = (row * rows) + column + 1 if row == rows - 1 && column == rows - 1 { _board[row].append(.empty) } else { _board[row].append(.number(number)) } } } } 38 — @basthomas
  24. init(rows: Int) { precondition(rows > 1, "A puzzle should at

    least be 2x2") var _board: [[Tile]] = [] for row in 0..<rows { _board.append([]) for column in 0..<rows { let number = (row * rows) + column + 1 if row == rows - 1 && column == rows - 1 { _board[row].append(.empty) } else { _board[row].append(.number(number)) } } } } 38 — @basthomas
  25. init(rows: Int) { precondition(rows > 1, "A puzzle should at

    least be 2x2") var _board: [[Tile]] = [] for row in 0..<rows { _board.append([]) for column in 0..<rows { let number = (row * rows) + column + 1 if row == rows - 1 && column == rows - 1 { _board[row].append(.empty) } else { _board[row].append(.number(number)) } } } } 38 — @basthomas
  26. @discardableResult mutating func solve() -> Solution<Board> { var boards: [Board]

    = [] repeat { next() boards.append(self) } while isSolved == false return Solution(steps: boards.map(Solution.Step.init)) } 46 — @basthomas
  27. private func adjacentPositions(to position: Position) -> [Position] { var adjacentPositions:

    [Position] = [] var positions: [Position] { return initialBoard .flatMap { $0 } .map(position(for:)) } /// ??? precondition( adjacentPositions.count <= 4, "Can't have more than four adjacent positions, got \(adjacentPositions)" ) precondition( adjacentPositions.count >= 2, "Must have at least two adjacent positions, got \(adjacentPositions)" ) return adjacentPositions } 47 — @basthomas
  28. private func adjacentPositions(to position: Position) -> [Position] { var adjacentPositions:

    [Position] = [] var positions: [Position] { return initialBoard .flatMap { $0 } .map(position(for:)) } /// ??? precondition( adjacentPositions.count <= 4, "Can't have more than four adjacent positions, got \(adjacentPositions)" ) precondition( adjacentPositions.count >= 2, "Must have at least two adjacent positions, got \(adjacentPositions)" ) return adjacentPositions } 47 — @basthomas
  29. private func adjacentPositions(to position: Position) -> [Position] { var adjacentPositions:

    [Position] = [] var positions: [Position] { return initialBoard .flatMap { $0 } .map(position(for:)) } /// ??? precondition( adjacentPositions.count <= 4, "Can't have more than four adjacent positions, got \(adjacentPositions)" ) precondition( adjacentPositions.count >= 2, "Must have at least two adjacent positions, got \(adjacentPositions)" ) return adjacentPositions } 47 — @basthomas
  30. private func adjacentPositions(to position: Position) -> [Position] { var adjacentPositions:

    [Position] = [] var positions: [Position] { return initialBoard .flatMap { $0 } .map(position(for:)) } /// ??? precondition( adjacentPositions.count <= 4, "Can't have more than four adjacent positions, got \(adjacentPositions)" ) precondition( adjacentPositions.count >= 2, "Must have at least two adjacent positions, got \(adjacentPositions)" ) return adjacentPositions } 47 — @basthomas
  31. private func adjacentPositions(to position: Position) -> [Position] { var adjacentPositions:

    [Position] = [] var positions: [Position] { return initialBoard .flatMap { $0 } .map(position(for:)) } /// ??? precondition( adjacentPositions.count <= 4, "Can't have more than four adjacent positions, got \(adjacentPositions)" ) precondition( adjacentPositions.count >= 2, "Must have at least two adjacent positions, got \(adjacentPositions)" ) return adjacentPositions } 47 — @basthomas
  32. for loopingPosition in positions where position != loopingPosition { switch

    loopingPosition { case Position(row: position.row, column: position.column - 1): fallthrough // above case Position(row: position.row - 1, column: position.column): fallthrough // left case Position(row: position.row + 1, column: position.column): fallthrough // below case Position(row: position.row, column: position.column + 1): precondition(adjacentPositions.contains(loopingPosition) == false) adjacentPositions.append(loopingPosition) // right default: continue // no match } } 48 — @basthomas
  33. for loopingPosition in positions where position != loopingPosition { switch

    loopingPosition { case Position(row: position.row, column: position.column - 1): fallthrough // above case Position(row: position.row - 1, column: position.column): fallthrough // left case Position(row: position.row + 1, column: position.column): fallthrough // below case Position(row: position.row, column: position.column + 1): precondition(adjacentPositions.contains(loopingPosition) == false) adjacentPositions.append(loopingPosition) // right default: continue // no match } } 48 — @basthomas
  34. for loopingPosition in positions where position != loopingPosition { switch

    loopingPosition { case Position(row: position.row, column: position.column - 1): fallthrough // above case Position(row: position.row - 1, column: position.column): fallthrough // left case Position(row: position.row + 1, column: position.column): fallthrough // below case Position(row: position.row, column: position.column + 1): precondition(adjacentPositions.contains(loopingPosition) == false) adjacentPositions.append(loopingPosition) // right default: continue // no match } } 48 — @basthomas
  35. for loopingPosition in positions where position != loopingPosition { switch

    loopingPosition { case Position(row: position.row, column: position.column - 1): fallthrough // above case Position(row: position.row - 1, column: position.column): fallthrough // left case Position(row: position.row + 1, column: position.column): fallthrough // below case Position(row: position.row, column: position.column + 1): precondition(adjacentPositions.contains(loopingPosition) == false) adjacentPositions.append(loopingPosition) // right default: continue // no match } } 48 — @basthomas
  36. for loopingPosition in positions where position != loopingPosition { switch

    loopingPosition { case Position(row: position.row, column: position.column - 1): fallthrough // above case Position(row: position.row - 1, column: position.column): fallthrough // left case Position(row: position.row + 1, column: position.column): fallthrough // below case Position(row: position.row, column: position.column + 1): precondition(adjacentPositions.contains(loopingPosition) == false) adjacentPositions.append(loopingPosition) // right default: continue // no match } } 48 — @basthomas
  37. for loopingPosition in positions where position != loopingPosition { switch

    loopingPosition { case Position(row: position.row, column: position.column - 1): fallthrough // above case Position(row: position.row - 1, column: position.column): fallthrough // left case Position(row: position.row + 1, column: position.column): fallthrough // below case Position(row: position.row, column: position.column + 1): precondition(adjacentPositions.contains(loopingPosition) == false) adjacentPositions.append(loopingPosition) // right default: continue // no match } } 48 — @basthomas
  38. mutating func shuffle(moves: Int = 50) { for _ in

    1...moves { let adjacentToEmpty = adjacentPositions(to: position(for: .empty)) precondition( adjacentToEmpty.count >= 2, "Should always have at least two positions adjacent to empty" ) // Remove the previously moved tile, so we do not move a tile // back-and-forth. That would be rather pointless. let adjacentWithoutPrevious = adjacentToEmpty .filter { $0 != position(for: _previouslyShuffledTile) } let randomAdjacent = adjacentWithoutPrevious.randomElement()! let randomTile = tile(at: randomAdjacent) move(tile: randomTile) _previouslyShuffledTile = randomTile } } 49 — @basthomas
  39. mutating func shuffle(moves: Int = 50) { for _ in

    1...moves { let adjacentToEmpty = adjacentPositions(to: position(for: .empty)) precondition( adjacentToEmpty.count >= 2, "Should always have at least two positions adjacent to empty" ) // Remove the previously moved tile, so we do not move a tile // back-and-forth. That would be rather pointless. let adjacentWithoutPrevious = adjacentToEmpty .filter { $0 != position(for: _previouslyShuffledTile) } let randomAdjacent = adjacentWithoutPrevious.randomElement()! let randomTile = tile(at: randomAdjacent) move(tile: randomTile) _previouslyShuffledTile = randomTile } } 49 — @basthomas
  40. mutating func shuffle(moves: Int = 50) { for _ in

    1...moves { let adjacentToEmpty = adjacentPositions(to: position(for: .empty)) precondition( adjacentToEmpty.count >= 2, "Should always have at least two positions adjacent to empty" ) // Remove the previously moved tile, so we do not move a tile // back-and-forth. That would be rather pointless. let adjacentWithoutPrevious = adjacentToEmpty .filter { $0 != position(for: _previouslyShuffledTile) } let randomAdjacent = adjacentWithoutPrevious.randomElement()! let randomTile = tile(at: randomAdjacent) move(tile: randomTile) _previouslyShuffledTile = randomTile } } 49 — @basthomas
  41. mutating func shuffle(moves: Int = 50) { for _ in

    1...moves { let adjacentToEmpty = adjacentPositions(to: position(for: .empty)) precondition( adjacentToEmpty.count >= 2, "Should always have at least two positions adjacent to empty" ) // Remove the previously moved tile, so we do not move a tile // back-and-forth. That would be rather pointless. let adjacentWithoutPrevious = adjacentToEmpty .filter { $0 != position(for: _previouslyShuffledTile) } let randomAdjacent = adjacentWithoutPrevious.randomElement()! let randomTile = tile(at: randomAdjacent) move(tile: randomTile) _previouslyShuffledTile = randomTile } } 49 — @basthomas
  42. mutating func shuffle(moves: Int = 50) { for _ in

    1...moves { let adjacentToEmpty = adjacentPositions(to: position(for: .empty)) precondition( adjacentToEmpty.count >= 2, "Should always have at least two positions adjacent to empty" ) // Remove the previously moved tile, so we do not move a tile // back-and-forth. That would be rather pointless. let adjacentWithoutPrevious = adjacentToEmpty .filter { $0 != position(for: _previouslyShuffledTile) } let randomAdjacent = adjacentWithoutPrevious.randomElement()! let randomTile = tile(at: randomAdjacent) move(tile: randomTile) _previouslyShuffledTile = randomTile } } 49 — @basthomas
  43. BRANCH... AND BOUND ▸ What are the options? ▸ Is

    there a best option? 51 — @basthomas
  44. BRANCH... AND BOUND ▸ What are the options? ▸ Is

    there a best option? ▸ Do it again 51 — @basthomas
  45. mutating func next() { let currentBoard = self.currentBoard let adjacentToEmpty

    = adjacentPositions(to: position(for: .empty)) let boardOptions = adjacentToEmpty.map { position -> (board: [[Tile]], moved: Tile, validPositions: Int) in let tileToMove = tile(at: position) move(tile: tileToMove) defer { self.currentBoard = currentBoard } return (self.currentBoard, tileToMove, validPositions) }.filter { $0.moved != _previousNextTile } let amountOfValidPositionsList = boardOptions.map { $0.validPositions } let largestAmountOfValidPositions = amountOfValidPositionsList.max() precondition(largestAmountOfValidPositions != nil, "No maximum valid positions found in \(boardOptions)") let nextBestSteps = amountOfValidPositionsList.filter { $0 == largestAmountOfValidPositions } let bestStepIndex = boardOptions.firstIndex { $0.validPositions == nextBestSteps.first } precondition(bestStepIndex != nil, "Should always have an index for the next best step") let bestOption = boardOptions[bestStepIndex!] self.currentBoard = bestOption.board _previousNextTile = bestOption.moved } 52 — @basthomas
  46. let boardOptions = adjacentToEmpty.map { position -> (board: [[Tile]], moved:

    Tile, validPositions: Int) in let tileToMove = tile(at: position) move(tile: tileToMove) defer { self.currentBoard = currentBoard } return (self.currentBoard, tileToMove, validPositions) }.filter { $0.moved != _previousNextTile } 54 — @basthomas
  47. let amountOfValidPositionsList = boardOptions.map { $0.validPositions } let largestAmountOfValidPositions =

    amountOfValidPositionsList.max() precondition( largestAmountOfValidPositions != nil, "No maximum valid positions found in \(boardOptions)" ) 55 — @basthomas
  48. let nextBestSteps = amountOfValidPositionsList.filter { $0 == largestAmountOfValidPositions } let

    bestStepIndex = boardOptions.firstIndex { $0.validPositions == nextBestSteps.first } precondition(bestStepIndex != nil, "Should always have an index for the next best step") let bestOption = boardOptions[bestStepIndex!] self.currentBoard = bestOption.board _previousNextTile = bestOption.moved 56 — @basthomas
  49. | 1| 2| 3| 4| | 5|10| 6|11| | 9|14|

    8| 7| |13| |15|12| 58 — @basthomas
  50. VALID POSITIONS ▸ 13 (right) > 7 ▸ 14 (down)

    > 9 ▸ 15 (left) > 7 60 — @basthomas
  51. VALID POSITIONS ▸ 13 (right) > 7 ▸ 14 (down)

    > 9 ▸ 15 (left) > 7 ▸ ... and repeat 60 — @basthomas