1. 程式人生 > 其它 >Python:DFS/BFS/UCS解決八皇后問題

Python:DFS/BFS/UCS解決八皇后問題

技術標籤:我的程式八皇后DFSBFSUCSPython

文章目錄


1 八皇后問題

有一個8乘8的棋盤,現在要將八個皇后放到棋盤上,滿足:對於每一個皇后,在自己所在的行、列、兩個對角線都沒有其他皇后。
在這裡插入圖片描述
規定初始狀態為【空棋盤】,動作為【每次只在最左面未放置皇后的列上放一個皇后】。這樣就使得棋盤的同列最多隻能出現一個皇后。


2 程式程式碼

2.1 程式1

程式1:functions.py。包括2個函式:attacked_queens_pairs,display_board,分別完成【計算序列對應棋盤的互相攻擊的皇后對數】和【列印輸出序列對應的棋盤】的功能。如下:

import numpy as np

def attacked_queens_pairs(seqs):
    """
    計算序列對應棋盤的【互相攻擊的皇后對數n】
    只需要檢查當前棋盤的八個皇后在各自的行和兩條對角線上是否有其他皇后,不需判斷同列是否有其他皇后
    """
    a = np.array([0] * 81)  # 建立一個有81個0的一維陣列
    a = a.reshape(9, 9)  # 改為9*9二維陣列。為方便後面使用,只用後八行和後八列的8*8部分,作為一個空白棋盤
    n = 0  # 互相攻擊的皇后對數初始化為0
for i in range(1, 9): if seqs[i-1] != 0: # seqs的某一元素為0代表對應棋盤的該列不應該放置任何皇后 a[seqs[i - 1]][i] = 1 # 根據序列,按從第一列到最後一列的順序,在空白棋盤對應位置放一個皇后,生成當前序列對應的棋盤 for i in range(1, 9): if seqs[i - 1] == 0: continue # seqs的某一元素為0代表著對應的棋盤該列未放置任何皇后,直接進行下一列的判斷 for k in
list(range(1, i)) + list(range(i + 1, 9)): # 檢查每個皇后各自所在的行上是否有其他皇后 if a[seqs[i - 1]][k] == 1: # 有其他皇后 n += 1 t1 = t2 = seqs[i - 1] for j in range(i - 1, 0, -1): # 看左半段的兩條對角線 if t1 != 1: t1 -= 1 if a[t1][j] == 1: n += 1 # 正對角線左半段上還有其他皇后 if t2 != 8: t2 += 1 if a[t2][j] == 1: n += 1 # 次對角線左半段上還有其他皇后 t1 = t2 = seqs[i - 1] for j in range(i + 1, 9): # 看右半段的兩條對角線 if t1 != 1: t1 -= 1 if a[t1][j] == 1: n += 1 # 正對角線右半段上還有其他皇后 if t2 != 8: t2 += 1 if a[t2][j] == 1: n += 1 # 次對角線右半段上還有其他皇后 return int(n/2) # 返回n/2,因為A攻擊B也意味著B攻擊A,因此返回n的一半 def display_board(seqs): """ 顯示序列對應的棋盤 """ board = np.array([0] * 81) # 建立一個有81個0的一維陣列 board = board.reshape(9, 9) # 改變為9*9二維陣列,為了後面方便使用,只用後八行和後八列的8*8部分,作為一個空白棋盤 for i in range(1, 9): board[seqs[i - 1]][i] = 1 # 根據序列,從第一列到最後一列的順序,在對應位置放一個皇后,生成當前序列對應的棋盤 print('對應棋盤如下:') for i in board[1:]: for j in i[1:]: print(j, ' ', end="") # 有了end="",print就不會換行 print() # 輸出完一行後再換行,這裡不能是print('\n'),否則會換兩行 print('攻擊的皇后對數為' + str(attacked_queens_pairs(seqs)))

此程式無任何輸出,只是定義了2個函式以供主程式呼叫。

2.2 程式2

2.2.1 DFS(深度優先搜尋)

深度優先搜尋(DFS, Depth-first search),總是擴充套件最深層的節點。DFS使用的是LIFO佇列,即使用的是stack棧。且是在生成節點時做的goal test。

程式2:main.py。為主程式,通過呼叫程式1的三個函式,完成DFS解決八皇后問題的全過程。如下:

import random
import time
from functions import attacked_queens_pairs, display_board

start = time.time()
frontier_stack = [[0] * 8] # 使用棧去儲存未擴充套件的葉子節點;初始狀態為8個0,代表棋盤上無皇后
solution = []
flag = 0 # 代表還未找到解

while frontier_stack: # 若frontier集中還有元素就繼續擴充套件,除非找到解則成功,或集合為空代表失敗
    if flag == 1: # 找到解就退出迴圈
        break
    seqs = frontier_stack.pop(-1) # LIFO,先擴充套件最新加入棧的序列
    nums = list(range(1, 9))  # 元素為1-8的列表
    for j in range(8): # 在序列中第一個為0的位置,即最左未放置皇后的列中挑選一行放置皇后
        pos = seqs.index(0)
        temp_seqs = list(seqs)
        temp = random.choice(nums)  # 在該列隨機挑選一行放置皇后
        temp_seqs[pos] = temp # 將皇后放在該列的第temp行
        nums.remove(temp)  # 從nums移除已產生的值
        if attacked_queens_pairs(temp_seqs) == 0:  # 將皇后放在該列的第temp行後,若序列對應棋盤無互相攻擊的皇后,則將序列儲存到frontier集
            frontier_stack.append(temp_seqs)
            if 0 not in temp_seqs: # 生成節點時做goal test:若序列中無0元素,即八個皇后均已放好,則序列為解序列
                solution = temp_seqs
                flag = 1 # 成功
                break

if solution:
    print('已找到解序列:' + str(solution))
    display_board(solution)
else:
    print('演算法失敗,未找到解')

end = time.time()
print('用時' + str('%.2f' % (end-start)) + 's')

一種輸出如下:

已找到解序列:[4, 7, 5, 2, 6, 1, 3, 8]
對應棋盤如下:
0  0  0  0  0  1  0  0  
0  0  0  1  0  0  0  0  
0  0  0  0  0  0  1  0  
1  0  0  0  0  0  0  0  
0  0  1  0  0  0  0  0  
0  0  0  0  1  0  0  0  
0  1  0  0  0  0  0  0  
0  0  0  0  0  0  0  1  
攻擊的皇后對數為0
用時0.01s

2.2.2 BFS(寬度優先搜尋)

寬度優先搜尋(BFS, Breadth-first search),總是擴充套件最淺層的節點,使用FIFO queue來記錄Frontier集。請注意目標結點一經生成,則它一定是最淺的目標結點,原因是所有比它的淺的結點在此之前已經生成並且肯定未能通過目標測試。但是最淺的目標結點不一定就是最優的目標結點;從技術上看,如果路徑代價是基於結點深度的非遞減函式,寬度優先搜尋是最優的。最常見的情況就是當所有的行動要花費相同的代價,八皇后問題就是這樣,每一次放置皇后的成本都一樣,沒有區別。因此在生成節點時做goal test是合適的。

程式2:main.py。為主程式,通過呼叫程式1的三個函式,完成BFS解決八皇后問題的全過程。如下:

import random
import time
from functions import attacked_queens_pairs, display_board

start = time.time()
frontier_queue = [[0] * 8] # 使用佇列去儲存未擴充套件的葉子節點;初始狀態為8個0,代表棋盤上無皇后
solution = []
flag = 0 # 代表還未找到解

while frontier_queue: # 若frontier集中還有元素就繼續擴充套件,除非找到解則成功,或集合為空代表失敗
    if flag == 1:
        break
    seqs = frontier_queue.pop(0) # FIFO,先擴充套件最先加入佇列的序列
    nums = list(range(1, 9))  # 元素為1-8的列表
    for j in range(8): # 在序列中第一個為0的位置,即最左未放置皇后的列中挑選一行放置皇后
        pos = seqs.index(0)
        temp_seqs = list(seqs)
        temp = random.choice(nums)  # 在該列隨機挑選一行放置皇后
        temp_seqs[pos] = temp # 將皇后放在該列的第temp行
        nums.remove(temp)  # 從nums移除已產生的值
        if attacked_queens_pairs(temp_seqs) == 0:  # 將皇后放在該列的第temp行後,若序列對應棋盤無互相攻擊的皇后,則將序列儲存到frontier集
            frontier_queue.append(temp_seqs)
            if 0 not in temp_seqs:  # 生成節點時做goal test:若序列中無0元素,即八個皇后均已放好,則序列為解序列
                solution = temp_seqs
                flag = 1  # 成功
                break
if solution:
    print('已找到解序列:' + str(solution))
    display_board(solution)
else:
    print('演算法失敗,未找到解')

end = time.time()
print('用時' + str('%.2f' % (end-start)) + 's')

一種輸出如下:

已找到解序列:[2, 7, 5, 8, 1, 4, 6, 3]
對應棋盤如下:
0  0  0  0  1  0  0  0  
1  0  0  0  0  0  0  0  
0  0  0  0  0  0  0  1  
0  0  0  0  0  1  0  0  
0  0  1  0  0  0  0  0  
0  0  0  0  0  0  1  0  
0  1  0  0  0  0  0  0  
0  0  0  1  0  0  0  0  
攻擊的皇后對數為0
用時1.25s

2.2.3 UCS(一致代價搜尋)

一致代價搜尋(UCS,Uniform-cost search),總能找到最優解,因為UCS總是先擴充套件【從初始節點到自己cost最小】的那個節點,且演算法停止條件為找到解或者Frontier集為空。UCS會在node的資料結構中記錄從初始節點到當前節點的cost,並按照Frontier集中每個節點的cost大小,從小到大將未擴充套件的節點排序,即Frontier集的資料結構為優先順序佇列(Priority Queue)。

UCS在擴充套件節點前做goal test,因為第一次生成目標節點時的路徑可能不是最優解。而且,在每次生成曾經已經出現的節點node時,先做的是比較【從初始狀態到node對應的狀態的舊路徑的cost】與【從初始狀態到node對應的狀態的新路徑的cost】的大小關係:若後者更小,則捨棄舊的路徑,替換為新路徑;若前者更小,則拋棄當前生成的節點node代表的路徑,舊路徑保持不變。

程式2:main.py。為主程式,通過呼叫程式1的三個函式,完成UCS解決八皇后問題的全過程。如下:

import random
import time
from functions import attacked_queens_pairs, display_board

start = time.time()
frontier_priority_queue = [{'cost':0, 'seqs':[0] * 8}] # 使用優先順序佇列去儲存未擴充套件的葉子節點;初始狀態為8個0,代表棋盤上無皇后,cost=0
solution = []
flag = 0 # 代表還未找到解

while frontier_priority_queue: # 若frontier集中還有元素就繼續擴充套件,除非找到解則成功,或集合為空代表失敗
    seqs = frontier_priority_queue.pop(0)['seqs'] # 先擴充套件cost最小的序列;由於每次都會按cost將各個序列從小到大排序,所以擴充套件選擇的是第一個序列
    if 0 not in seqs: # 擴充套件節點前做goal test:若序列中無0元素,即八個皇后均已放好,則序列為解序列
        solution = seqs
        flag = 1  # 成功
        break
    nums = list(range(1, 9))  # 元素為1-8的列表
    cost = 1 # 八皇后問題中每次放置皇后的代價相同,故cost即為深度,也就是放置皇后的個數
    for j in range(8): # 在序列中第一個為0的位置,即最左未放置皇后的列中挑選一行放置皇后
        pos = seqs.index(0)
        temp_seqs = list(seqs)
        temp = random.choice(nums)  # 在該列隨機挑選一行放置皇后
        temp_seqs[pos] = temp # 將皇后放在該列的第temp行
        nums.remove(temp)  # 從nums移除已產生的值
        if attacked_queens_pairs(temp_seqs) == 0:  # 將皇后放在該列的第temp行後,若序列對應棋盤無互相攻擊的皇后,則將序列儲存到frontier集
            frontier_priority_queue.append({'cost':cost,'seqs':temp_seqs})
    frontier_priority_queue = sorted(frontier_priority_queue, key=lambda x:x['cost'])
    cost += 1

if solution:
    print('已找到解序列:' + str(solution))
    display_board(solution)
else:
    print('演算法失敗,未找到解')

end = time.time()
print('用時' + str('%.2f' % (end-start)) + 's')

一種輸出如下:

已找到解序列:[3, 1, 7, 5, 8, 2, 4, 6]
對應棋盤如下:
0  1  0  0  0  0  0  0  
0  0  0  0  0  1  0  0  
1  0  0  0  0  0  0  0  
0  0  0  0  0  0  1  0  
0  0  0  1  0  0  0  0  
0  0  0  0  0  0  0  1  
0  0  1  0  0  0  0  0  
0  0  0  0  1  0  0  0  
攻擊的皇后對數為0
用時1.55s

當所有的單步耗散都相等的時候,一致代價搜尋與寬度優先搜尋類似,除了演算法終止條件,寬度優先搜尋在找到解時終止(因為是在生成節點後做goal test,成功則終止演算法),而一致代價搜尋則會檢查目標深度的所有結點看誰的代價最小(因為是在擴充套件節點前做goal test),但是實質上目標深度的結點代價都相等,均為8(因為擺放的是8個皇后);這樣,在這種情況下一致代價搜尋無意義地做了更多的工作,因此執行時間比寬度優先搜尋長。

UCS演算法在複雜問題上雖然總可以找到最優解,但是有時候走的路徑比較繞遠,即時間複雜度與空間複雜度上表現較差。


3 評價

因為DFS是先擴充套件最新加入frontier集的那個節點(LIFO),因此很快就可以將八個皇后放在合適的位置;而BFS與UCS需要將frontier集中的所有節點(即序列)對應的棋盤都放好7個皇后後,再繼續擴充套件frontier中節點,生成的節點才可能通過goal test,這樣就耗費了很多時間;與BFS生成節點時做goal test相比,UCS選擇在擴充套件節點前做goal test,導致UCS在frontier集中已有多個符合條件的解後還在繼續生成解,在擴充套件前才發現自己已經有滿足條件的解,所以UCS比BFS的執行時間要長一點。

因此,根據三種演算法的平均執行時間,DFS、BFS、UCS解決八皇后問題的效率與效能依次降低。


END