DEV Community

vc7
vc7

Posted on

LeetCode in Swift - 773. Sliding Puzzle (中文解釋)

題目

2024/11/25 的每日一題


資料結構

  • BFS / 廣度優先搜尋
  • Backtracking / 回溯法

題意

方法會傳入一個二維陣列,這裡就叫他數字板。我們必須要判斷這個二維陣列在多次移動後會不會成為以下的排列。可以的話,回傳移動的次數;否則回傳 -1

[[1, 2, 3], [4, 5, 0]] 
Enter fullscreen mode Exit fullscreen mode

想法

處理方式

Concept

根據題意得知

  • 需要透過列舉出各種移動結果,來看哪個結果符合我們的預期。
    • 對策:
      • 當需要 列舉所有可能性 ,就可以使用 backtracking / 回溯法。
      • 根據探索的方式,發現可以用廣度優先搜尋為基底,根據接下來的需求進行變形
  • 由於只有空白 (0) 的地方可以被移動進去,因此每一次移動都是以 0 的位置為基準
    • 在每一次的走訪後需要紀錄 0 新的位置
  • 根據題意,必須回傳「移動過幾次」
    • 因此當每一次生成新的數字板時,必須記錄當下的深度
    • 一次深度代表一次的移動
  • 由於移動過程中 board 的排列會重複,因此當遇到重複的時候需要跳過
    • 需要有個 visited Set 來紀錄已經走訪過的 board 排列

資料結構整形

因為二維陣列不容易進行存取和更新,考慮到易讀性,在開始處理前可以降一個維度成為一維陣列,進行 swapping 時的寫法會比較簡潔。

[[1, 2, 3], -> [1, 2, 3, 4, 0, 5] [4, 0, 5]] 
Enter fullscreen mode Exit fullscreen mode
let board = board.reduce(into: [Int]()) { $0.append(contentsOf: $1 )} 
Enter fullscreen mode Exit fullscreen mode

探索路線

在 backtracking 列舉的過程中,我們必須要根據能和當下 0 交換的位置生成新的數字板。

二維陣列對照到一維陣列的 indices

 [0][1][2] [0][1][2][3][4][5] ----------- ------------------ [[1, 2, 3], -> [1, 2, 3, 4, 0, 5] [4, 0, 5]] ----------- [3][4][5] 
Enter fullscreen mode Exit fullscreen mode

路線表 / 對照表

0 的位置 能交換的位置 二維陣列中的相對位置
[0] 1, 3 右、下
[1] 0, 2, 4 左、右、下
[2] 1, 5 左、下
[3] 0, 4 上、右
[4] 1, 3, 5 上、左、右
[5] 2, 4 上、左

為了易讀性的資料結構

根據以上的說明可以知道每一次的走訪需要三個資訊

  • 0 的位置
  • 數字板的樣子
  • 當前深度或移動次數

為了易讀性,與其使用大於等於 3 個元素的 tuple ,我宣告了一個結構:

struct Item { /// 0 的位置 let index: Int /// 數字板的樣子 let board: [Int] /// 當前深度或移動次數 let depth: Int } 
Enter fullscreen mode Exit fullscreen mode

Routine

在文章一開始有提到會使用 BFS 為基礎演算法解題,因此會有一個主要的 queue 來暫存每一層列舉的結果。

準備

  1. Queue - 儲存列舉的結果。用來進行 BFS 走訪的主資料結構。
  2. Visited - 儲存已經走訪過的數字板排列。

走訪過程

取出 queue ("dequeue") 中第一個物件來處理。

提前結束條件

當前物件的 board 為預期結果時,回傳當前的深度(移動次數)。

列舉

  1. 根據當前的 index 從「路線表」找出對應的新位置。
  2. 根據所有新位置生成新的數字板
    • 當新的數字板有在 visited 出現過,跳過不處理
    • 沒出現過的話
      • 把新的 0 的位置、數字板樣式、加 1 後的深度組合起來存入 queue
      • 把新的數字板樣式存入 visited

走訪完成 - Queue 為空

當這個 queue 為空時,代表沒有沒有達成「提前結束條件」,也就意味著傳入的二維陣列無論如何移動都無法達成 [1, 2, 3, 4, 5, 0] ,因此回傳 -1


程式碼

接下來就可以根據以上的說明和 routine 來寫成程式碼了:

class Solution { /// 路線圖。因為這是靜態不需要改變,因此可以宣告為 member property 。 private let routes = [ [1, 3], [0, 2, 4], [1, 5], [0, 4], [1, 3, 5], [2, 4] ] /// 預期結果。因為這是靜態不需要改變,因此可以宣告為 member property 。 private let target = [1, 2, 3, 4, 5, 0] /// LeetCode 的進入點 func slidingPuzzle(_ board: [[Int]]) -> Int { // MARK: - 1. 初始處理 /// 把二維陣列降為一維陣列,作為初始數字板 let board = board.reduce(into: [Int]()) { $0.append(contentsOf: $1 )} /// 取得初始數字板 0 的所在位置 /// 根據題目限制這邊理應不會失敗,若失敗了視為無法達成題目要求回傳 -1 。 guard let index = board.firstIndex(of: 0) else { return -1 } // MARK: - 2. BFS 的準備 var queue = [Item(index: index, board: board, depth: 0)] var visited: Set<[Int]> = [board] // MARK: - 3. Routine while !queue.isEmpty { let item = queue.removeFirst() guard item.board != target else { return item.depth } // 根據新位置列舉 for next in routes[item.index] { var board = item.board board[next] = item.board[item.index] board[item.index] = item.board[next] // 只處理未出現過的數字板 if !visited.contains(board) { queue.append(Item(index: next, board: board, depth: item.depth + 1)) visited.insert(board) } } } // MARK: - 4. End of Routine return -1 } // MARK: Data Type struct Item { let index: Int let board: [Int] let depth: Int } } 
Enter fullscreen mode Exit fullscreen mode

結語

這題難是難在拆解題意,和怎麼把題意套進 BFS 來解。因為需要講解清楚的環節很多,所以花了很多時間想該怎麼寫會比較好。

以上,如果有什麼回饋,歡迎在留言區跟我說,謝謝!

Top comments (0)