1. 程式人生 > 其它 >Python:GBF/A*演算法解決八皇后問題

Python:GBF/A*演算法解決八皇后問題

技術標籤:我的程式八皇后GBFA星演算法Python

文章目錄


1 八皇后問題

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

不瞭解GBF/A*演算法的話,請看這裡


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 GBF(貪婪最佳優先搜尋)

貪婪最佳優先搜尋(Greedy best-first search,GBF)。GBF先擴充套件與目標最近的節點,因為這樣可能最快找到解。因此,GBF只使用啟發式資訊(heuristic function,h(n),n為節點),即f(n)=h(n),在擴充套件節點前做goal test。

程式2:main.py。為主程式,通過呼叫程式1的二個函式,完成GBF解決八皇后問題的全過程。程式碼中,h(n)=互相攻擊的皇后對數。如下:

import random
import time
from functions import attacked_queens_pairs, display_board

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

while frontier_priority_queue: # 若frontier非空就繼續迴圈,若成功找到解則跳出迴圈輸出解,若frontier為空時還未找到解則宣告失敗
    first = frontier_priority_queue.pop(0)  # 先擴充套件h(n)最小的序列;由於每次都會按h(n)將各個序列從小到大排序,所以擴充套件第一個序列
    seqs = first['seqs']
    if 0 not in seqs: # 擴充套件節點前做goal test:若序列中無0元素,即八個皇后均已放好,則序列為解序列
        solution = seqs
        flag = 1  # 成功
        break
    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移除已產生的值
        frontier_priority_queue.append({'pairs':attacked_queens_pairs(temp_seqs),'seqs':temp_seqs})
    frontier_priority_queue = sorted(frontier_priority_queue, key=lambda x:x['pairs'])

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

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

輸出如下:

已找到解序列:[8, 3, 1, 6, 2, 5, 7, 4]
對應棋盤如下:
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  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
用時4.53s

耗時較多…

2.2.2 A*演算法(或稱為A星演算法)

A*演算法中f(n)=g(n)+h(n),在擴充套件節點前做goal test。

程式2:main.py。為主程式,通過呼叫程式1的二個函式,完成A*演算法解決八皇后問題的全過程。程式碼中,g(n)=未放置好的皇后個數,h(n)=互相攻擊的皇后對數。如下:

import random
import time
from functions import attacked_queens_pairs, display_board

start = time.time()
frontier_priority_queue = [{'unplaced_queens':8, 'pairs':28, 'seqs':[0] * 8}] # 初始狀態為8個0,代表棋盤上無皇后;g(n)=未放置好的皇后個數,h(n)=互相攻擊的皇后對數,初始設h(n)=28,g(n)=8
solution = []
flag = 0 # 代表還未找到解

while frontier_priority_queue: # 若frontier非空就繼續迴圈,若成功找到解則跳出迴圈輸出解,若frontier為空時還未找到解則宣告失敗
    first = frontier_priority_queue.pop(0)  # 由於每次都會將frontier排序,所以擴充套件的是第一個序列
    if first['pairs'] == 0 and first['unplaced_queens'] == 0: # 擴充套件節點前做goal test:若序列h(n)=g(n)=0,則序列為解序列
        solution = first['seqs']
        flag = 1  # 成功
        break
    nums = list(range(1, 9))  # 元素為1-8的列表
    seqs = first['seqs']
    if seqs.count(0) == 0: # 由於後面程式碼中的排序機制可能將【8個皇后已放好,即g(n)=0,但互相攻擊的皇后對數接近於0,但是不為0,即h(n)!=0】的節點放在首位;而此類節點肯定不符合要求,但是這樣的節點是無法擴充套件的,因為8個皇后已經放完了
        continue # 只能跳過這種節點
    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移除已產生的值
        frontier_priority_queue.append({'unplaced_queens':temp_seqs.count(0), 'pairs':attacked_queens_pairs(temp_seqs),'seqs':temp_seqs})
    frontier_priority_queue = sorted(frontier_priority_queue, key=lambda x:(x['pairs']+x['unplaced_queens']))

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

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

輸出如下:

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

需要注意的是,上述程式碼在大部分情況下執行時間很少,但是由於frontier排序機制的原因,有時候執行時間會達到半分鐘甚至超過1分鐘。為什麼?

A*演算法中f(n)=g(n)+h(n),按f(n)從小到大對frontier集排序,這樣簡單的相加有時會將大量【8個皇后已放好,即g(n)=0,但互相攻擊的皇后對數接近於0,但是不為0,即h(n)!=0】的節點放在首位或者前面,而此類節點肯定不符合要求,但是這樣的節點是無法擴充套件的,因為8個皇后已經放完了,只能跳過該節點,進行下一節點的判斷。這樣的【大量】,這樣的【有時】,會導致有時需要大量的執行時間,即執行時間不穩定。

下面對排序機制進行修改。

2.2.3 對A*演算法程式碼的微小改動

程式2:main.py。為主程式,將2.2.2節的程式2程式碼修改了下:1、將18、19行的程式碼刪去,因為不再需要那樣的判斷條件;2、將27行的排序條件進行了修改——先按h(n)從小到大的順序將序列排序;若h(n)相同,則按g(n)從小到大的順序將序列排序。如下:

import random
import time
from functions import attacked_queens_pairs, display_board

start = time.time()
frontier_priority_queue = [{'unplaced_queens':8, 'pairs':28, 'seqs':[0] * 8}] # 初始狀態為8個0,代表棋盤上無皇后;g(n)=未放置好的皇后個數,h(n)=互相攻擊的皇后對數,初始設h(n)=28,g(n)=8
solution = []
flag = 0 # 代表還未找到解

while frontier_priority_queue: # 若frontier非空就繼續迴圈,若成功找到解則跳出迴圈輸出解,若frontier為空時還未找到解則宣告失敗
    first = frontier_priority_queue.pop(0)  # 由於每次都會將frontier排序,所以擴充套件的是第一個序列
    if first['pairs'] == 0 and first['unplaced_queens'] == 0: # 擴充套件節點前做goal test:若序列h(n)=g(n)=0,則序列為解序列
        solution = first['seqs']
        flag = 1  # 成功
        break
    nums = list(range(1, 9))  # 元素為1-8的列表
    seqs = first['seqs']
    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移除已產生的值
        frontier_priority_queue.append({'unplaced_queens':temp_seqs.count(0), 'pairs':attacked_queens_pairs(temp_seqs),'seqs':temp_seqs})
    frontier_priority_queue = sorted(frontier_priority_queue, key=lambda x:(x['pairs'], x['unplaced_queens'])) # 先按h(n)從小到大的順序將序列排序;若h(n)相同,則按g(n)從小到大的順序將序列排序

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

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

輸出如下:

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

修改frontier排序機制後的好處顯而易見——執行時間穩定在1s內。可見,在使用A*演算法解決問題時,要不要死板地將g(n)和h(n)簡單相加為f(n),然後根據f(n)排序,是個值得考慮的問題。

注意,本節修改後的程式碼已經不能算A*演算法的程式碼了。


3 評價

相對於GBF演算法,A*演算法程式碼的執行時間更少,畢竟A*演算法是全域性擇優且用到了更多有用的資訊。


END