八皇后問題 來自於西方象棋(現在叫 國際象棋,英文chess),詳情可見 百度百科。
在西方象棋中,有一種叫做皇后的棋子,在棋盤上,如果雙方的皇后在同一行、同一列或同一斜線上,就會互相攻擊。
八皇后問題:
在8行8列的棋盤上擺放8個皇后,使之不能互相攻擊——任意兩個不在同一行、同一列或同一斜線上。
Level 1:找到一種擺放的方法
Level 2:找到總共有多少種方法
----------
下面展示在《Python基礎教程》(第二版·修訂版)中看到的解法,本文的目的是對其進行解讀,加深自己的理解(今天花了一個多鍾想明白)。
應用到的重要技術:生成器、遞歸算法
解決方法:
Step 1-定義沖突函數
在當前行的8個位置擺放棋子時,檢查是否與已擺放的棋子是否沖突。
def conflict(state, nextX): #state為已擺放的棋子的位置的元組,比如(1,3,0) nextY = len(state) #獲取當前行號 for i in range(nextY): #檢查每一個已擺放的棋子和當前行要擺放的棋子的位置(nextX, nextY)是否沖突 if abs(state[i] - nextX) in (0, nextY - i): #關鍵!若是兩個棋子 行差值 的絕對值 出現在元組 (0,行差值)中,則沖突發生,返回True return True return False #無沖突,可行的擺放位置
Step 2-遞歸實現尋找擺放方案
八皇后問題,是否可以拆解為七皇后問題,再拆解為六皇后問題,再……一皇后問題?可以。不過,一皇后、二皇后、三皇后問題都是沒有解決方案的。
在擺放最后一個棋子時,前面的棋子已經沒有沖突了,那么,最后一步,依次檢查在最后一行的每個位置擺放棋子是否和已擺放的棋子是否沖突,如果
不沖突,那么,一種解決方案就有了——遞歸的終結(書中叫做 基本情況)。
def queens(num=8, state=()): #num為棋盤的行數,state為已擺放的棋子的列數匯總,類型為 元組 if len(state) == num - 1: #檢查是否最后一行,如果是最后一行,則執行終極操作,不再遞歸 for pos in range(num): #pos從 0 到 棋盤行數 - 1 if not conflict(state, pos): #檢查是否沖突yield (pos,) #沒有沖突,返回列數的元組 else: #不是最后一行 for pos in range(num): #檢查當前行每一個列的位置 if not conflict(state, pos): #檢查是否有沖突for result in queens(num, state + (pos,)): #遞歸調用queens,不過找到了更多的一行的擺放位置,所以,加上(pos,)
#如果是最后一行,返回一個數字的元組,比如,(2)
#此時,如果pos為0,那么,倒數第二行返回的為兩個數字的元組,比如,(0, 2)
#調用每返回一層,返回的元組的長度就加1,直到最后用戶在外部調用queens的位置
#返回所有行的皇后位置的 元組,其長度為行數 yield (pos,) + result #這里和想象的有些不同,下面是在Python 2.7.14上運行代碼發生的錯誤
#程序執行后沒問題,可是~~看來還沒想明白!(下面有進一步分析)
queens(...)返回的是一個迭代器(生成器 更准確!生成器就是一種迭代器!),因此,下面代碼中的result是一個元組,而不是一個數值:
for result in queens(num, state + (pos,))
這樣,(pos,) + result就解釋的通了!啊哈,明白了!
可以在yield語句上添加打印語句,可以更好地幫助分析。上面想不通的原因在於,將queens(...)返回的內容的類型搞錯了,也是不了解
生成器的原理所致,現在理解更深刻啦。
代碼改進:
在上面第二份代碼中,兩個分支中的紅色部分是完全相同的,因此,可以對代碼進行簡化,下面是簡化結果。
def queens2(num=8, state=()): for pos in range(num): #重復部分 if not conflict(state, pos): #重復部分 if len(state) == num - 1: #分支1 yield (pos,) else: #分支2 for result in queens(num, state + (pos,)): yield (pos,) + result
執行結果:
----------
在2.7.14中執行下面的代碼發生錯誤了:試驗上面搞不明白的問題