作者自我介紹:大爽歌, b站小UP主 ,
python1對1輔導老師,
時常直播程式設計,直播時免費回答簡單問題。
前置知識: 遞迴演算法(recursion algorithm)。
我的遞迴教程: 【教程】python遞迴三部曲(基於turtle實現視覺化)
回溯與遞迴的關係:
回溯是一種演算法思想,遞迴是實現方式。
回溯法經典問題:
八皇后問題、數獨問題。
(其實兩個很像)
八皇后問題
八皇后問題是一個以國際象棋為背景的問題:
如何在8×8的國際象棋棋盤上放置八個皇后,使其不互相攻擊。
即任兩個皇后都不能處於同一條橫行、縱行或斜線上。
n皇后問題
八皇后問題可以推廣為更一般的n皇后擺放問題:這時棋盤的大小變為n×n,而皇后個數也變成n。
(當且僅當n = 1 或 n ≥ 4時問題有解)
4皇后問題!
八皇后討論起來比較麻煩,先討論四皇后情況(n=4)
首先展示下錯誤的情況:
如上圖所示,三個圖的錯誤分別是
- 第一行有重複了
- 對角線有重複了。(注意有兩個對角線)
- 第一列有重複
想要正確,則每一行每一列,每個對角線(對角線有兩個方向)都不能有重複項。
正確的情況示例如下:
回溯法
回溯法(backtracking)是暴力搜尋法中的一種。
其核心思想就是不斷嘗試,不行就後退再試其他的。
關於這一思想,我之前有個視訊,感覺能比較形象地展示,感興趣可以看看:
接下來我們用回溯法探究下剛才的4皇后問題。
回溯法過程展示
個人感覺用行列座標表示不夠直觀,所以給每個格子從前往後依次編號。
後面用編號來稱呼位置(無特殊說明的話)
如下圖
同時四個皇后從前往後按次序編為
\(Q_1\)、\(Q_2\)、\(Q_3\)、\(Q_4\)
原始的回溯法
每次會從前往後依次嘗試每個編號的位置。
為了簡化談論,以下先進行了一定的優化。
由於每行不能重複,n個皇后必須分別放在n行上。
當有一行放不下了時。也就失敗了。
所以
\(Q_1\)必須放在第一行(行索引為0)
\(Q_2\)必須放在第二行(行索引為1)
\(Q_3\)必須放在第三行(行索引為2)
\(Q_4\)必須放在第四行(行索引為3)
1 \(Q_1\)放位置0
使用回溯法,\(Q_2\)仍然會從0開始嘗試,發現放不了,就往後走。
由於\(Q_1\)放位置0。所以
0、1、2、3、
4、8、12、
5、10、15都放不了
\(Q_2\)從第二行開頭試。
即從4、5開始試,一直試到6才能夠放下,那麼就先放在這裡。
\(Q_2\)放位置6
那麼接下來繼續嘗試\(Q_3\),
會發現第三行(行索引為2)已經放不了了。
如下圖
這說明
\(Q_2\)放位置6失敗
回來重新放\(Q_2\),放位置7
\(Q_2\)放位置7
此時\(Q_3\)唯一能放的位置只有9。
之後,\(Q_4\)已經無處可放。
如下圖
這說明
\(Q_2\)放位置7失敗
\(Q_2\)無位置可放
\(Q_2\)無位置可放,
說明\(Q_1\)放在位置0失敗。
\(Q_1\)需要嘗試其他位置,即嘗試先放在位置1。
到這裡回溯法的特點其實就已經展現的比較夠了:
即不斷向下嘗試,如果所有嘗試都失敗,那就後退一步,重新嘗試。
2 \(Q_1\)放位置1
此時\(Q_2\)只能放在位置7,
之後\(Q_3\)只能放在位置8,
最後\(Q_3\)只能放在位置14,
即如下圖所示
到這裡,如果只要求找到一個解法,問題就已經結束了,如果要找到所有解法,那就是繼續往後不斷嘗試。
程式碼實現
原始回溯法程式碼
class NQueens:
def __init__(self, n):
self.n = n
# 儲存每個皇后的座標, (ci, ri)
# 第一行第一列的皇后座標為(0, 0)
self.one_solution = []
def check_can_place(self, ri, ci):
for pos in self.one_solution:
pc, pr = pos
if pc == ci: # 行檢測
return False
if pr == ri: # 列檢測
return False
if pr - pc == ri - ci: # 對角線檢測 1
return False
if pr + pc == ri + ci: # 對角線檢測 2
return False
return True
def solve(self):
for ri in range(self.n):
for ci in range(self.n):
if self.check_can_place(ri, ci):
pos = (ci, ri)
self.one_solution.append(pos)
if len(self.one_solution) == self.n:
return True
res = self.solve()
if res:
return True
else:
self.one_solution.pop()
return False
def show_in_board(self):
board = [
["-" for i in range(self.n)] for j in range(self.n)
]
for pos in self.one_solution:
pc, pr = pos
board[pr][pc] = "Q"
for row in board:
print(" ".join(row))
nq = NQueens(8)
res = nq.solve()
if res:
print("Queens positions:")
print(nq.one_solution)
print("Queens in board:")
nq.show_in_board()
輸出結果
Queens positions:
[(0, 0), (4, 1), (7, 2), (5, 3), (2, 4), (6, 5), (1, 6), (3, 7)]
Queens in board:
Q - - - - - - -
- - - - Q - - -
- - - - - - - Q
- - - - - Q - -
- - Q - - - - -
- - - - - - Q -
- Q - - - - - -
- - - Q - - - -
check_can_place
方法
該方法,用於檢查指定的橫縱座標,是否還能防止皇后(不與已經放置的皇后衝突)
檢查是否能放置
行和列好分析,對角線情況則比較麻煩。
兩種對角線圖示如下
第一種對角線(紅色對角線)
每一條對角線上格子,\(r-c\)都是相同的值。
可以通過這個值來判斷是否在同一條對角線上。
第二種對角線(綠色對角線)
每一條對角線上格子,\(r+c\)都是相同的值。
可以通過這個值來判斷是否在同一條對角線上。
solve
方法解析
def solve(self):
for ri in range(self.n):
for ci in range(self.n):
# 從前往後嘗試所有的位置,看是否能放皇后
if self.check_can_place(ri, ci):
# 成功則新增
pos = (ci, ri)
self.one_solution.append(pos)
if len(self.one_solution) == self.n:
# 皇后數量已到達n,問題解決,返回解決成功
return True
# 走到這裡,說明還沒解決
# 遞迴呼叫自身,看當前情況往後是否能夠解決成功
res = self.solve()
if res:
# 成功,就繼續返回解決成功
return True
else:
# 失敗,之前新增的pos方法,是不成功的,將其彈出,之後繼續嘗試
self.one_solution.pop()
return False
程式碼優化與擴充
優化:一行一試
上面的原始回溯法的程式碼。
每一次放皇后都是從前往後一個一個試,效率很低。
這裡按照上文討論中的思路進行優化,
即每一行放一個皇后。
那麼程式碼裡面就是每一行,從第一列開始一直嘗試到最後一列。
一行放好後,就往下一行進行嘗試。
這裡只需要給NQueens
類新增一個新方法solve_advanced
即可
def solve_advanced(self, ri=0):
for ci in range(self.n):
if self.check_can_place(ri, ci):
pos = (ci, ri)
self.one_solution.append(pos)
if ri == self.n - 1:
return True
res = self.solve_advanced(ri+1)
if res:
return True
else:
self.one_solution.pop()
return False
呼叫時的res = nq.solve()
改成res = nq.solve_advanced()
即可。
輸出和原始回溯法時的輸出是一樣的。
不過程式碼執行的速度會得到很大提升。
不僅如此,優化後的程式碼在去求所有解時,不會求出重複情況。
擴充:獲得所有解(不重複)
求所有解的程式碼在優化後的方法上,簡單調整以下就好
- 不再返回(即不會試到一個成功的就退出)
- 成功後將結果記錄,記錄時要使用切片進行拷貝。
首先,先在NQueens
的__init__
方法中新增新的屬性,用於記錄解決方法。
self.solutions = []
然後給NQueens
類新增新方法solve_all
def solve_all(self, ri=0):
for ci in range(self.n):
if self.check_can_place(ri, ci):
pos = (ci, ri)
self.one_solution.append(pos)
if ri == self.n - 1:
self.solutions.append(self.one_solution[:])
else:
self.solve_all(ri+1)
self.one_solution.pop()
然後修改下show_in_board
方法。
因為原來的方法只能展示self.one_solution
。
這裡希望也能夠展示別的solution
修改後的show_in_board
如下
def show_in_board(self, sol=None):
board = [
["-" for i in range(self.n)] for j in range(self.n)
]
if sol is None:
sol = self.one_solution
for pos in sol:
pc, pr = pos
board[pr][pc] = "Q"
for row in board:
print(" ".join(row))
總程式碼
一個NQueens
的例項,只能呼叫三個方法中的一個(一次)
solve
solve_advanced
solve_all
重複呼叫可能會出問題(需要再呼叫,建議新建NQueens
例項)
以下總程式碼中只展示solve_all
的呼叫結果。
且由於八皇后問題的解太多(有92個),
以下只展示下六皇后問題的呼叫求解
class NQueens:
def __init__(self, n):
self.n = n
# 儲存每個皇后的座標, (ci, ri)
# 第一行第一列的皇后座標為(0, 0)
self.one_solution = []
self.solutions = [
]
def check_can_place(self, ri, ci):
for pos in self.one_solution:
pc, pr = pos
if pc == ci: # 行檢測
return False
if pr == ri: # 列檢測
return False
if pr - pc == ri - ci: # 對角線檢測 1
return False
if pr + pc == ri + ci: # 對角線檢測 2
return False
return True
def solve(self):
for ri in range(self.n):
for ci in range(self.n):
if self.check_can_place(ri, ci):
pos = (ci, ri)
self.one_solution.append(pos)
if len(self.one_solution) == self.n:
return True
res = self.solve()
if res:
return True
else:
self.one_solution.pop()
return False
def solve_advanced(self, ri=0):
for ci in range(self.n):
if self.check_can_place(ri, ci):
pos = (ci, ri)
self.one_solution.append(pos)
if ri == self.n - 1:
return True
res = self.solve_advanced(ri+1)
if res:
return True
else:
self.one_solution.pop()
return False
def solve_all(self, ri=0):
for ci in range(self.n):
if self.check_can_place(ri, ci):
pos = (ci, ri)
self.one_solution.append(pos)
if ri == self.n - 1:
self.solutions.append(self.one_solution[:])
else:
self.solve_all(ri+1)
self.one_solution.pop()
def show_in_board(self, sol=None):
board = [
["-" for i in range(self.n)] for j in range(self.n)
]
if sol is None:
sol = self.one_solution
for pos in sol:
pc, pr = pos
board[pr][pc] = "Q"
for row in board:
print(" ".join(row))
nq = NQueens(6)
solutions = nq.solve_all()
for si in range(len(nq.solutions)):
sol = nq.solutions[si]
print("=== Solution %s ===" % si)
print("Queens positions:")
print(sol)
print("Queens in board:")
nq.show_in_board(sol)
輸出
總程式碼的輸出如下
=== Solution 0 ===
Queens positions:
[(1, 0), (3, 1), (5, 2), (0, 3), (2, 4), (4, 5)]
Queens in board:
- Q - - - -
- - - Q - -
- - - - - Q
Q - - - - -
- - Q - - -
- - - - Q -
=== Solution 1 ===
Queens positions:
[(2, 0), (5, 1), (1, 2), (4, 3), (0, 4), (3, 5)]
Queens in board:
- - Q - - -
- - - - - Q
- Q - - - -
- - - - Q -
Q - - - - -
- - - Q - -
=== Solution 2 ===
Queens positions:
[(3, 0), (0, 1), (4, 2), (1, 3), (5, 4), (2, 5)]
Queens in board:
- - - Q - -
Q - - - - -
- - - - Q -
- Q - - - -
- - - - - Q
- - Q - - -
=== Solution 3 ===
Queens positions:
[(4, 0), (2, 1), (0, 2), (5, 3), (3, 4), (1, 5)]
Queens in board:
- - - - Q -
- - Q - - -
Q - - - - -
- - - - - Q
- - - Q - -
- Q - - - -