回溯法
在瞭解八皇后問題之前我們先了解什麼是回溯法,因爲八皇后問題是回溯法的一個經典算法習題,也是八皇后問題用到的主要算法。
根據百度百科解釋:回溯法(探索與回溯法)是一種選優搜索法,又稱爲試探法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術爲回溯法,而滿足回溯條件的某個狀態的點稱爲“回溯點”。
舉個集合小例子:列舉集合 {1,2,3} 中所有子集的問題
使用回溯法。從集合的開頭元素開始,對每個元素都有兩種操作,直到集合最後一個元素。其中的每個操作都可以看作是一次嘗試,每次嘗試都可以得出一個結果。將得到的結果綜合起來,就是集合的所有子集。
回溯法和遞歸的聯繫:
遞歸是從問題的結果出發,例如求 n!,要想知道 n!的結果,就需要知道 n*(n-1)! 的結果,而要想知道 (n-1)! 結果,就需要提前知道 (n-1)*(n-2)!。這樣不斷地向自己提問,不斷地調用自己的思想就是遞歸。
回溯和遞歸唯一的聯繫就是,回溯法可以用遞歸思想實現
八皇后問題
八皇后問題是以國際象棋爲背景的問題:有八個皇后(可以當成八個棋子),如何在 8*8 的棋盤
中放置八個皇后,使得任意兩個皇后都不在同一條橫線、縱線或者斜線上。
算法思路:
- 從棋盤的第一行開始,從第一個位置開始,依次判斷當前位置是否能夠放置皇后。
判斷的依據爲:同該行之前的所有行中的皇后所在位置進行比較,如果在同一列,或者在同一條對角線上(正方形的兩條對角線),都不符合條件,繼續檢查後序位置; - 如果i該行所有位置都不符合要求,則回溯到前一行,改變皇后的位置,繼續i試探;
- 如果試探到最後一行,所有皇后位置擺放完畢,則直接打印出8*8棋盤。最後將棋盤恢復原樣,避免影響下一次擺放。
實現代碼:
代碼分爲三部分:
- conflict 函數,判斷下一行皇后位置是否與之前皇后的位置衝突
- queens函數,採用生成器的方式來產生每一個皇后的位置,並用遞歸思想實現回溯法計算出每一種結果的皇后的位置
- prettyprint函數,友好展示棋盤,畫出每一種結果的皇后的位置
def conflict(state, nextColumn):
"""
判斷是否衝突
因爲座標是從0開始的,所以state的長度代表了下一行的行座標
:param state:(7,4,6,0,2) 標記每行皇后所在的位置 (0,7)一行八列 (2,4) (3,6) (4,0) (5,2)
:param nextColumn:下一行的列座標
:return:
"""
nextRow = rows = len(state) # 5
for row in range(rows): # 0,1,2,3,4
# 獲取當前行的列
column = state[row]
"""
如何判斷是否衝突:
1. 如果列的差值爲0,說明兩皇后在同一列
2. 如果列的差值等於行的差值,說明兩皇后在對角線上
"""
if abs(column - nextColumn) in [0, nextRow - row]:
return True
return False
# 採用生成器的方式來產生每一個皇后的位置,並用遞歸來實現下一個皇后的位置
def queens(num, state=()):
"""
基於遞歸採用回溯算法,算出每一種結果
:param num: 皇后的數量 8
:param state: 列座標。初始爲空。參數爲元組不爲列表,因爲參數只能爲不可變數據類型
:return:
"""
# 每一行的列座標都是從0:7的
# 0,1,2,3,4,5,6,7
for pos in range(num):
# 默認state爲空。長度爲0,但是是不衝突的
# 判斷是否衝突,state爲空時不衝突
if not conflict(state, pos): # 回溯法的體現
# 如果state的長度爲7,即到達了倒數第二行,也就是前7行皇后都已經找到了位置,最後一行又沒有衝突,返回最後一行的列座標
if len(state) == num - 1:
# 最後一行的(pos,)=最後一行的result,然後再遞歸回去求倒數第二行的result
yield (pos,)
else:
for result in queens(num, state + (pos,)):
"""
遞歸實現求state:
1. 向下遞歸
第一次(行): pos=0,剛開始不會進入if len(state) == num - 1,進入執行else,會執行queens(num, state + (pos, )),
第二次(行): 進入else,再調用queens(num, state + (pos, )),遞歸執行queens(num, state + (pos,) + (pos,))
第三次(行): 進入else,再調用queens(num, state + (pos,) + (pos,),遞歸執行queens(num, state + (pos,) + (pos,) + (pos,))
...
第七次(行): 執行和上面的一樣,不過此時state的長度爲7
第八次(行): 執行f len(state) == num - 1:求出最後一行的列座標(pos,)
2.向上遞歸
求出第八行的列座標,就可以求出第七行的(pos,),返回的是第七行和第八行的列座標((pos,) + result)
根據下一行的結果依次求出上一行的結果;
....
最後求出第一行的列座標,返回整體結果
"""
yield (pos,) + result
def prettyprint(solution):
"""
進行友好展示:爲了至關表現棋盤,用X表示皇后的位置
:param solution:
:return:
"""
def line(pos, length=len(solution)):
return '.' * (pos) + 'X' + '.' * (length - pos -1)
for pos in solution:
print(line(pos))
if __name__ == '__main__':
solutions = queens(8)
for index, solution in enumerate(solutions):
print('第%d種解決方案:' %(index + 1), solution )
prettyprint(solution)
print('*' * 50)
結果展示:
第1種解決方案: (0, 4, 7, 5, 2, 6, 1, 3)
X.......
....X...
.......X
.....X..
..X.....
......X.
.X......
...X....
**************************************************
第2種解決方案: (0, 5, 7, 2, 6, 3, 1, 4)
X.......
.....X..
.......X
..X.....
......X.
...X....
.X......
....X...
第91種解決方案: (7, 2, 0, 5, 1, 4, 6, 3)
.......X
..X.....
X.......
.....X..
.X......
....X...
......X.
...X....
**************************************************
第92種解決方案: (7, 3, 0, 2, 5, 1, 6, 4)
.......X
...X....
X.......
..X.....
.....X..
.X......
......X.
....X...
**************************************************