N皇后問題—回溯演算法經典例題
題目描述:
N 皇后是回溯演算法經典問題之一。問題如下:請在一個 n×n 的正方形盤面上佈置 n 名皇后,因為每一名皇后都可以自上下左右斜方向攻擊,所以需保證每一行、每一列和每一條斜線上都只有一名皇后。
題目分析:
在 N 皇后問題中,回溯演算法思路是每一次只佈置一個皇后,如果盤面可行,就繼續佈置下一個皇后。一旦盤面陷入死局,就返回一步,調整上一個皇后的位置。重複以上步驟,如果解存在,我們一定能夠找到它。可以看到,我們在重複“前進—後退—前進—後退”這一過程。問題是,我們不知道一共需要重複這個過程多少次,也不能提前知道 n 是多少,更不知道每一次後退時需要後退幾行,因此我們不能利用 for 迴圈和 while 迴圈來實現這個演算法。因此我們需要利用遞迴來實現程式碼結構。邏輯如下:當方法佈置完當前行的皇后,就讓方法呼叫自己去佈置下一行的皇后。當盤面變成絕境的時候,就從當前方法跳出來,返回到上一行,換掉上一行的皇后再繼續。
我們定義 NQueens(n) 方法,它負責輸出所有成立的 n×n 盤面。其中 1 代表皇后,0 代表空格。
程式碼:
def NQueens(n): #輸出所有成立的n·n盤面
cols = [0 for _ in range(n)] #每一行皇后的縱座標
res = [] #結果列表
def checkBoard(rowIndex): #檢查盤面是否成立,rowIndex是當前行數
for i in range(rowIndex):
if cols[i]==cols[rowIndex]: #檢查豎線
return False
if abs(cols[i]-cols[rowIndex]) == rowIndex-i: #檢查斜線
return False
return True
def helper(rowIndex): #佈置第rowIndex行到最後一行的皇后
if rowIndex==n: #邊界條件
board = [[0 for _ in range(n)] for _ in range(n)]
for i in range(n):
board[i][cols[i]] = 1
res.append(board) #把當前盤面加入結果列表
return #返回
for i in range(n): #依次嘗試當前行的空格
cols[rowIndex] = i
if checkBoard(rowIndex): #檢查當前盤面
helper(rowIndex+1) #進入下一行
helper(0) #從第1行開始
return res
print(NQueens(4))
程式碼分析:
在 NQueens() 方法中,我們會定義 helper(x) 方法幫助實現遞迴結構。helper(x) 方法負責佈置第 x 行到最後一行的皇后,它在佈置完當前行的皇后之後會呼叫自己,接著佈置剩餘行的皇后。遞迴的邊界條件是當 x 等於 n 時,也就是盤面已經完整的時候,helper() 方法會把當前的盤面加進結果列表。
在解題過程中,我們需要一個變數用於儲存當前盤面解,我們會動態地修改這個變數直到盤面滿足條件。我們有兩種儲存方式,第一個是宣告一個 n×n 的二維陣列,初始是全部為 0,在解題過程中放置 1。第二種儲存方式是利用一個長度為 n 的一維陣列,其中第 i 個數值代表第 i 行皇后的座標。比如 [1,0,2,3] 所表示的與上面的二維陣列一致。因為兩種方式儲存的資訊量一樣,但是第二個選項佔用更少的空間,所以我們優先選擇第二個選項。
在 NQueens() 方法中我們定義了兩個子方法:checkBoard() 和 helper()。我們直接呼叫 helper(0),讓 helper() 方法幫我們解決問題。最後直接輸出 res 答案集合。
首先注意要在呼叫 checkBoard() 和 helper() 前定義它們,否則程式會報錯。另外一種寫法是將這兩個方法放在 NQueens()
外面。但是在方法中定義子方法的好處是我們可以直接呼叫 cols,n,res這些存在於 NQueens() 方法範圍內的變數。如果把子方法定義在
NQueens() 外面,這些變數必須作為引數傳入子方法,寫起來相對比較煩瑣。
我們宣告以下兩個變數的用途如下:
- cols:這是我們當前的題解,第 i 個數值代表第i行皇后的座標。比如 [1,0,2,4] 代表第一行的皇后在第二個空格,第二行的皇后在第一個空格,以此類推。注意座標總是需要減 1;
- rowIndex:我們所處的當前行的行數。我們從第 1 行開始時,rowIndex 等於 0,當 helper(0) 呼叫 helper(1) 後,我們進入到了第 2 行,所以 rowIndex 在那時等於 1。
在 checkBoard() 方法裡,我們做了兩個檢查,第一個是看每一列是不是隻有一個皇后,第二個是看每一條斜線上是不是隻有一個皇后。因為我們每多新增一名皇后時都會做一遍檢查,所以我們在添加當前皇后前,盤面肯定是成立的。因此,我們只需要看一下當前皇后對盤面的影響是否成立。也就是說,我們只需要檢查當前皇后的縱座標是不是獨一無二的,以及當前皇后的兩條斜線上有沒有其他皇后就可以。斜線檢查利用了縱座標差和橫座標差的絕對值。
helper() 方法通過 for 迴圈依次嘗試當前行的 n 個空格。如果把皇后放在第一個空格里成立,它會直接呼叫自己去往下一行(rowIndex+1 行),但是如果當前空格不成立,它會嘗試下一個空格。
如果 n 個空格都不成立,它會後退到上一行的 for 迴圈裡,繼續嘗試上一行的下一個空格。比如,如果第三行的 n 個空格都不成立,helper(2) 就會返回 helper(1),把第二個皇后換到第二行的下一個空格。