八皇后問題分析和 golang 求解
問題:在一個8*8大小的國際象棋棋盤上放置8個皇后棋子,使得所有的皇后都是安全的即任意兩個皇后都無法攻擊到對方)。
分析:
按照國際象棋的規則,皇后的攻擊方式是橫,豎和斜向。
皇后可以攻擊到同一列所有其它棋子,因此可推匯出每1列只能存在1個皇后,即每個皇后分別佔據一列。棋盤一共8列,剛好放置8個皇后。
為了擺放出滿足條件的8個皇后的佈局,可以按如下方式逐步操作:
1. 在第0列找到一個位置放置第1個皇后; 2. 在第1列找到一個位置放置第2個皇后; 3. 在第2列找到一個位置放置第3個皇后; 4. 對第3,4,5,6,7,8列都執行放置操作; 5. 當執行完“在第7列找到一個放置第8個皇后”這一操作完畢後,問題求解完畢。
觀察1,2,3,4 這些操作步驟,可以發現是一個重複的動作,抽象一下,即“在第 n 列放下第 n+1 個皇后”,n 從0到7。看起來我們需要寫一個這樣的過程:
func put(col int)
顯然,需要使得 col 從0到7每次+1,一共執行8次 put。
可以考慮用迴圈或著遞迴來完成它,由於一般來說,使用遞迴解決問題的思路會更加自然和簡單,因此我們選擇使用遞迴。那麼 put 看起來可能是這樣的:
func put(col int) { // 執行某些操作 put(cor+1) }
我們猜測遞迴的啟動方式可能是從 col = 0 開始,那麼呼叫 put(0)
作為程式入口的 main 函式看起來是這樣的:
func main() { put(0) }
接下來我們需要考慮遞迴如何結束。分析步驟5可知,當 col==8 時遞迴結束。因此我們繼續寫 put 的程式碼。
func put(col int) { if col == 8 { return } // 執行某些操作 put(col+1) }
遞迴程式的框架看起來已經搭好了,接下來我們要求解的子問題是
在第 col 列,找到一個位置,放下第 col+1 個皇后。
這個子問題的約束條件是:
放下皇后之後,棋盤上沒有任意兩個皇后可以攻擊到對方,即我們找到的這個位置是安全的。
由於每次放下皇后時,只有8個位置可選,因此在某行放下皇后的操作步驟如下:
1. 在第0行放下皇后; 2. 檢測當前位置是否安全; 3. 如果安全,則子問題求解結束; 4. 如果不安全,則在下一個位置放皇后; 5. 回到步驟3,繼續。
看起來這是一個迴圈問題,寫下程式碼:
for pos = 0; pos < 8; pos++ { // 在 pos 處放下皇后 if safe() { // 子問題求解結束 } }
第3步找到安全位置後,我們就去後一列找放置下一個皇后的位置了,即接下來呼叫 put(col+1),程式碼如下:
for pos := 0; pos < 8; pos++ { // 在 pos 處放下皇后 if safe() { put(col+1) } }
現在看一下目前的程式碼。
func put(col int) { if col == 8 { return } for pos := 0; pos < 8; po++ { // 在 pos 處放下皇后 if safe() { put(col+1) } } }
檢視程式碼之後我們發現,程式碼裡沒有任何東西來表示棋盤,以及皇后的位置。看起來我們需要做點什麼。
我們需要選用某個資料結構來表示棋盤和皇后的位置。
棋盤是由64個格子組成的,排列布局為8*8。第一反應是可以用一個二維陣列來表示,下標是格子的座標,陣列元素選用 bool 型, true 表示此處放置了皇后, false 表示空格子。
初始時,陣列中的64個元素都為 false, 程式執行之後,其中8個元素的值變為 true。
但看起來似乎有點浪費,而且處理多維陣列的下標比較麻煩。我們可以再想想,是否可以簡化一下棋盤的表示。
讓我們再用自然語言描述一下最終求出的解,也許是這樣的:
第0列的第a行有皇后 第1列的第b行有皇后 第2列的第c行有皇后 ... 第7列的第h行有皇后
一維陣列 [a, b, c, d, e, f, g, h] 就能表示上面這個解的全部資訊。不妨先用簡單的試試看。
對於 go 語言,slice 是比陣列好得多的選擇,我們用一個型別為 [8]int 的變數 boards 來表示棋盤。現在程式碼如下:
func put(col int) { if col == 8 { fmt.Println(boards) // 輸出答案 return } for pos := 0; pos < 8; po++ { boards[col] = pos // 在 pos 處放下皇后 if safe() { put(col+1) } } }
出於遵循“變數的作用域儘可能小”這一設計原則,我們把 boards 這個型別為[8]int 的 slice 作為 put 的引數。現在程式碼如下:
func put(boards [8]int, col int) { if col == 8 { fmt.Println(boards) // 輸出答案 return } for pos := 0; pos < 8; po++ { boards[col] = pos // 在 pos 處放下皇后 if safe() { put(boards, col+1) } } }
初始時,我們需要一個空的棋盤。在 main 函式再加點東西。
func main() { boards := [8]int{} put(boards, 0) }
有了可以表示棋盤的資料物件 boards 之後,我們來完成 safe 這個函式。
boards 作為 safe 的引數,接下來我們要檢驗棋盤上 pos 這個位置是否安全。
咋一看,我們可能想到遍歷整個棋盤,看看是否有任意兩個皇后可以攻擊。但我們再閱讀一下已經寫好的程式碼並仔細回顧最初的解題操作步驟,程式碼顯示我們的操作邏輯是:
1. 在第n行的 pos (pos從0到7) 放下皇后 2. 檢測 pos 是否安全 3. ...
我們再看看“外”一層的操作步驟:
1. 在第0列找到一個位置放置第1個皇后; 2. 在第1列找到一個位置放置第2個皇后; 3. ... 4. 在第6列找到一個位置方式第7個皇后; 5. ...
我們發現,按照這個操作步驟,當我們在第 n 列尋找安全格子 pos 時,我們只需要檢查這個 pos 是否會被前面列的那些皇后們攻擊到,而這些皇后們本身彼此都是不會互相攻擊。
那麼檢查的操作步驟如下:
1. 獲取第0列皇后的位置; 2. 檢查是否會攻擊到第n列的 pos; 3. 如果能攻擊到,則不安全; 4. 如果安全,則迴圈計數加1,回到步驟1繼續; 5. ... 6. 迴圈的的最後一次時檢查第 n-1 列皇后是否會攻擊到第n列的 pos,如果依然攻擊不到,那麼我們能確認 pos 是安全的。
因此,safe 函式的實現如下:
func safe(boards [8]int, col int, pos int) bool { for c := 0; c < col; c++ { if isAttack(boards, c, col, pos) { return false } } return true }
需要傳遞 boards , col 和 pos 三個引數。返回 bool 值,false 表示不安全, true 表示安全。
程式碼反映了一個事實:只要有一個皇后能攻擊到 pos ,則 pos 就是不安全的,只有檢車了前面 n-1 個皇后,它們全都不能攻擊 pos ,我們才可以說 pos 是安全的。
現在 put 的程式碼如下:
func put(boards [8]int, col int) { if col == 8 { fmt.Println(boards) // 輸出答案 return } for pos := 0; pos < 8; pos++ { boards[col] = pos // 在 pos 處放下皇后 if safe(boards, col, pos) { put(boards, col+1) } } }
接下來我們著手實現 isAttack。
func isAttack(boards [8]int, c int, col int, pos int) bool { if boards[c] == pos { return true } if pos-boards[c] == c-col { return true } if pos-boards[c] == col-c { return true } return false }
這個 function 需要4個引數讓我們嗅到了一點不好的味道,其中有3個引數都是從 safe 延續而來的,我們來看看呼叫 safe 的程式碼片段:
... boards[col] = pos // 在 pos 處放下皇后 if safe(boards, col, pos) { ... }
boards, col, pos 是存在關聯的,在執行完 boards[col] = pos 之後,boards[col] 的值就等於 pos 了,因此我們可以去掉 pos 這個引數,用 boards[col] 替換它。
最後再檢查一遍,程式碼裡多處出現8這個 magic number,這顯然不好。
8是棋盤的尺寸,即 boards 這個 slice 的長度,我們動手修改 boards 的型別和初始化方式。
新的初始化程式碼如下:
size := 8 boards := make([]int, size)
現在,magic number 可以用 len(boards) 來替代了。
我們意外地發現,這次重構後,程式可以求解任意尺寸棋盤的皇后問題了。
最終完整程式碼如下:
package main import "fmt" func main() { queen(8) } func queen(size int) { boards := make([]int, size) put(boards, 0) } func put(boards []int, col int) { size := len(boards) if col == size { fmt.Println(boards) // 輸出答案 return } for pos := 0; pos < size; pos++ { boards[col] = pos // 在 pos 處放下皇后 if safe(boards, col) { put(boards, col+1) } } } func safe(boards []int, col int) bool { for c := 0; c < col; c++ { if isAttack(boards, c, col) { return false } } return true } func isAttack(boards []int, c int, col int) bool { if boards[c] == boards[col] { return true } if boards[col]-boards[c] == c-col { return true } if boards[col]-boards[c] == col-c { return true } return false }