Python:GBF/A*算法解决八皇后问题


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

猜你喜欢

转载自blog.csdn.net/qq_40061206/article/details/112033438
今日推荐