1. 程式人生 > 程式設計 >python 使用遞歸回溯完美解決八皇后的問題

python 使用遞歸回溯完美解決八皇后的問題

八皇后問題描述:在一個8✖️8的棋盤上,任意擺放8個棋子,要求任意兩個棋子不能在同一行,同一列,同一斜線上,問有多少種解法。

規則分析:

任意兩個棋子不能在同一行比較好辦,設定一個佇列,佇列裡的每個元素代表一行,就能達到要求

任意兩個棋子不能在同一列也比較好處理,設定的佇列裡每個元素的數值代表著每行棋子的列號,比如(0,7,3),表示第一行的棋子放在第一列,第二行的棋子放在第8列,第3行的棋子放在第4列(從0開始計算列號)

任意兩個棋子不能在同一斜線上,可以把整個棋盤當作是一個XOY平面,原點在棋盤的左上角,斜線的斜率為1或者-1,X為列號,Y為行號,推出斜線的表示式為Y=X+n或者Y=-X+n(n為常數,斜線確定下來之後n就確定了),進而可以推匯出Y-X=n或者Y+X=n。也就是說在同一斜線上的兩個棋子行號與列號之和或者之差相等。X1+Y1=X2+Y2或者X1-Y1=X2-Y2。再進行變換能夠得到X1-X2=Y2-Y1或者X1-X2=Y1-Y2,也就是說|X1-Y1|=Y1-Y2。即判斷兩個棋子是否在同一斜線上,只要判斷出兩個棋子的列號之差是否等於兩個棋子的行號之差的絕對值就行了。

如下圖:

python 使用遞歸回溯完美解決八皇后的問題

將上述文字分析轉化為程式碼,就可以判斷棋子之間是否符合規則了(abs(num)表示取num的絕對值)

def is_rule(queen_tup,new_queen):
 """
 :param queen_tup: 棋子佇列,用於儲存已經放置好的棋子,數值代表相應棋子列號
 :param new_queen: 被檢測棋子,數值代表列號
 :return: True表示符合規則,False表示不符合規則
 """
 num = len(queen_tup)
 for index,queen in enumerate(queen_tup):
 
  if new_queen == queen: # 判斷列號是否相等
   return False
  if abs(new_queen-queen) == num-index: # 判斷列號之差絕對值是否與行號之差相等
   return False
 
 return True

事實上,這段代買還可以簡寫,判斷列號之差也可以寫作是列號之差是否為0,這樣就可以使用一個in來完成整個判斷。修改後如下

def is_rule(queen_tup,new_queen):
 """判斷棋子是否符合規則"""
 for index,queen in enumerate(queen_tup):
  if abs(new_queen-queen) in (len(queen_tup)-index,0): # 判斷表示式
   return False
 return True

接下來寫一下襬放棋子的函式

擺放棋子其實有兩種方法,第一種,求出8✖️8棋盤上每行放置一個棋子的所有方法,也就相當於全排列。然後再用衝突函式逐個判斷是否符合規則,如符合就放入佇列

第二種,在一行放入棋子,然後判斷是否符合規則,符合的情況下再去放下一行,下一行如果所有位置都不符合,退回到上一行,上一行的棋子再放置一個新的位置,然後再進去下一行判斷有沒有符合規則的棋子的位置。這種方法叫做遞歸回溯,每一行就相當於是一個回溯點

這裡我使用第二種方法寫個函式,先上程式碼,然後再解釋

def arrange_queen(num,queen_tup=list()):
 """
 :param num:棋盤的的行數,當然數值也等於棋盤的列數
 :param queen_tup: 設定一個空佇列,用於儲存符合規則的棋子的資訊
 """
 
 for new_queen in range(num): # 遍歷一行棋子的每一列
 
  if is_rule(queen_tup,new_queen): # 判斷是否衝突
 
   if len(queen_tup) == num-1:  # 判斷是否是最後一行
    yield [new_queen] # yield關鍵字
 
   else:
    # 若果不是最後一行,遞迴函式接著放置棋子
    for result in arrange_queen(num,queen_tup+[new_queen]):
     yield [new_queen] + result

如果能夠理解上邊函式的可以不用看下面的分析了,如果不明白,接下來我將舉幾個程式碼例子來說明上面的函式

首先是yield,這個是python裡的關鍵字,帶有yield的函式被稱作為生成器函式。函式在執行的時候,遇到yield關鍵字會暫停函式的執行,同時返回yield右邊的物件到函式被呼叫的地方,直到函式下次被執行,將回到yield所在的地方繼續執行,如果函式執行完畢還沒有遇到yield,就會丟擲一個異常StopIteration。而生成器函式需要使用next方法來執行。下面的程式碼將解釋生成器函式的執行:

def demo():
 
 yield 1
 yield 2
 print('end')
 
b = demo()  # 將生成器函式的引用傳遞給變數b
print(next(b)) # 第一次執行生成器函式,返回 1 同時函式暫停,列印結果
print(next(b)) # 第二次執行生成器函式,返回 2 同時函式暫停,列印結果
print(next(b)) # 第三次執行生成器函式,因為沒有再遇到yield,函式執行完畢,丟擲異常StopIteration

但是上述放置棋子的程式碼中並沒用呼叫next方法來執行生成器函式,而是使用了for迴圈遍歷,並且在函式執行完畢之後也沒有丟擲StopIteration的錯誤。那是因為for迴圈在執行的時候,會不斷的自動呼叫next方法,並且在遇到StopIteration的時候會捕捉異常並終止迴圈,以下程式碼我將模擬一下for迴圈來執行生成器函式

def demo():
 
 yield 1
 yield 2
 print('end')
 
 
# 模擬的for迴圈
b = demo()
while True:
 try:
  next(b)
  """
  此段區域寫for下的程式碼塊
  """
 except StopIteration:
  break
 
# 實際的for迴圈
for i in demo():
 """
 for 下的程式碼塊
 """
 pass

通過這個可以知道,當使用for迴圈驅動生成器函式的時候,如果函式執行完畢還沒有遇到yield關鍵字,就會直接退出for迴圈而不會執行for迴圈下的程式碼塊。值得注意的是,上邊兩個迴圈分別是呼叫了兩次生成器函式。生成器函式在一次執行完畢之後再繼續呼叫是不會得到結果的

瞭解了生成器函式與for迴圈是怎麼驅動生成器函式之後,關於棋子的遞迴函式裡面還有一個就是遞迴函數了。以前上課的時候老師將遞迴函式使用的例子是數值的階乘,這裡我也使用階乘來解釋一下遞迴函式的執行。先介紹一下階乘:給定一個正整數n,規定n的階乘n!=n(n-1)(n-2).....1。也就是從1到n的累乘。(0!=1,這是規定,別問我為什麼......)

def a(num):
 result = num*b(num-1)
 return result
 
 
def b(num):
 
 result = num*c(num-1)
 return result
 
 
def c(num):
 if num == 1:
  result = 1
 return result
 
 
result = a(3)
print(result)

上述程式碼是函式巢狀,只能用作計算3的階乘,我使用它來理解遞迴函式

a函式被呼叫執行的時候,傳參3,然後呼叫函式b,同時傳參3-1=2,函式b執行在呼叫函式c同時傳參2-1=1,函式c執行,判斷傳參結果符合,返回數值result到函式c被呼叫的地方,然後與b的引數2相乘,得到新的結果賦值給b裡面的result,然後再將result返回到b被呼叫的地方,再乘a的引數3賦值給a裡面的result,再將a裡的result返回到函式a被呼叫的地方,然後列印結果。

這就是利用函式的巢狀來執行出3!,那麼如果想算10000的函式呢?難道寫10000個函式?

這裡發現a函式和b函式除了變數名字不一樣,其餘的形式都一摸一樣,那麼直接在a裡面呼叫a函式,寫成如下形式

def a(num):
 result = num*a(num-1)
 return result


但是這樣的話,函式將不斷的被呼叫。所以加一個函式終止的條件,變成了

def a(num):
 if num == 1:
  return 1
 else:
  return num*a(num-1)
 
 
result = a(3)
print(result)

這就是一個最簡單的遞迴函式

分析函式的執行,函式第一次被呼叫,傳遞引數3,判斷不滿足終止條件。繼續執行,接下來再呼叫函式a,傳遞引數3-1=2,判斷不滿足終止條件。繼續執行,接下來再呼叫函式a,傳遞引數2-1=1,判斷滿足終止條件,第三次被呼叫的函式結束,返回1到被呼叫的地方,與2相乘,第二次被呼叫的函式結束,結果再返回到第二次函式被呼叫的地方,與3相乘,第一次被呼叫的函式結束,結果返回

這就是這個最簡單的遞迴函式的執行過程。總結就是遞迴函式不斷的呼叫自身,直至滿足函式終止的條件

搞定了含有yield的生成器函式,for迴圈驅動生成器函式的實質,遞迴函式的呼叫,我們再來看八皇后的棋子擺放的函式,為了方便觀察,將‘八皇后'改為‘四皇后',就是隻算4✖️4棋盤上放置4個棋子

def arrange_queen(num,queen_tup+[new_queen]):
     yield [new_queen] + result
 
 
for i in arrange_queen(4):
 print(i)

執行結果是

[1,3,2]

[2,1]

下面描述一下函式的執行過程:

1.放置第一行棋子。函式第一次被呼叫,傳遞引數4,空列表。放置棋子在第一行第一列,判斷棋子放置符合規則,判斷不是最後一行,將棋子位置資訊放入列表,同時生成新的列表[0]

2.放置第二行棋子。函式第二次被呼叫,傳遞引數4,列表[0]。放置棋子在第二行第一列,判斷棋子不符合規則,接著放置棋子在第二行第二列,判斷棋子不符合規則,再放置棋子在第二行第三列,判斷符合規則,將棋子位置資訊放入列表,同時生成新的列表[0,2]

3.放置第三行棋子。函式第三次被呼叫,傳遞引數4,列表[0,2]。放置棋子在第三行第一列,判斷棋子不符合規則,接著放置棋子在第三行第二列,判斷不符合規則,再放置棋子到第三行第三列,判斷不符合規則,再放置棋子到第三行第四列,判斷還是不符合規則。第三次函式呼叫結束

4.回到函式第二次被呼叫的地方,第二次被呼叫的函式接著放置棋子,上一次放置到了第三列,這次放到第四列,判斷符合規則,將棋子位置資訊放入列表,同時生成新的列表[0,3]

5.函式被呼叫,用於放置第三行,從第一列再依次判斷到最後一列,如果符合規則,放入棋子資訊,同時生成新的列表[0,1]

6.函式被呼叫,用於放置第四行,從第一列判斷到最後一列,都不符合規則,函式執行完畢,回到上一級

.......

N.當前三行的棋子放入都符合規則,而且第四行也符合規則了,此時第一次遇到yield關鍵字,第四級函式暫停,將棋子資訊放入列表[2],返回到第三級,第三級函式也將第三級符合規則的棋子資訊放入列表,同時與第四級返回的列表相加,得到一個新的列表,然後遇到第三級函式的關鍵字函式yield,第三級函式暫停,返回了[0,2]到第二級函式.......直到第一級函式暫停,返回結果[1,2],列印結果

然後第一級函式接著執行,驅動二級函式執行,二級驅動三級執行,三級驅動四級執行....

直到所有結果列印完畢,整個函式執行完畢

整個程式碼為

def is_rule(queen_tup,0): # 判斷表示式
   return False
 return True
 
 
def arrange_queen(num,queen_tup+[new_queen]):
     yield [new_queen] + result
 
 
for i in arrange_queen(8):
 print(i)

整個程式碼最終要的就是遞歸回溯的思想,如果能真正的明白,不用用什麼語法或者寫什麼樣的函式,都能輕鬆解決這個八皇后的問題

接下來我貼出一個八皇后的的終極版(下面的程式碼來源百度百科),不使用yield關鍵字的。可以自行理解一下

def queen(A,cur=0):
 if cur == len(A):
  print(A)
  return 0
 for col in range(len(A)):
  A[cur],flag = col,True
  for row in range(cur):
   if A[row] == col or abs(col - A[row]) == cur - row:
    flag = False
    break
  if flag:
   queen(A,cur+1)
queen([None]*8)

八皇后的所有解

[0,4,7,5,2,6,1,3]
[0,4]
[0,2]
[0,2]
[1,4]
[1,3]
[1,3]
[2,5]
[2,0]
[2,4]
[2,1]
[2,4]
[3,5]
[3,1]
[3,6]
[3,0]
[3,2]
[3,7]
[3,5]
[4,2]
[4,3]
[4,6]
[4,0]
[4,1]
[4,7]
[4,2]
[5,3]
[5,4]
[5,6]
[5,7]
[5,2]
[6,4]
[6,5]
[6,3]
[6,3]
[7,5]
[7,4]

最後最後,對比其他語言解決八皇后的程式碼量

以上這篇python 使用遞歸回溯完美解決八皇后的問題就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。