Python: GBF/A* algorithm solves the eight queens problem


1 The Eight Queens Problem

There is an 8-by-8 chessboard, and now you have to put eight queens on the chessboard, satisfying: for each queen, there is no other queen in the row, column, or two diagonals where it is.
Insert picture description here
It is stipulated that the initial state is [Empty Board], and the action is [Only put a queen on the leftmost column without a queen at a time]. In this way, at most one queen can appear in the same column of the chessboard.

If you don’t understand the GBF/A* algorithm, please see here .


2 program code

2.1 Procedure 1

Program 1: functions.py. It includes two functions: attacked_queens_pairs and display_board, which respectively complete the functions of [Calculating the number of pairs of mutually attacking queens corresponding to the sequence corresponding to the chessboard] and [Printing out the corresponding chessboard of the sequence]. as follows:

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)))

This program has no output, but two functions are defined for the main program to call.

2.2 Procedure 2

2.2.1 GBF (greedy best first search)

Greedy best-first search (Greedy best-first search, GBF). GBF first expands the node closest to the target, because this may find the solution the fastest. Therefore, GBF only uses heuristic information (heuristic function, h(n), n is the node), that is, f(n)=h(n), and does a goal test before expanding the node.

Program 2: main.py. As the main program, the whole process of GBF solving the eight queens problem is completed by calling two functions of program 1. In the code, h(n) = logarithm of queens attacking each other. as follows:

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')

The output is as follows:

已找到解序列:[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

More time consuming...

2.2.2 A* algorithm (or A-star algorithm)

In the A* algorithm, f(n)=g(n)+h(n), do a goal test before expanding the node.

Program 2: main.py. As the main program, the whole process of A* algorithm solving the eight queens problem is completed by calling two functions of program 1. In the code, g(n)=the number of unplaced queens, h(n)=the number of queens attacking each other. as follows:

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')

The output is as follows:

已找到解序列:[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

It should be noted that the above code runs very little in most cases, but due to the frontier sorting mechanism, sometimes the running time can reach half a minute or even more than 1 minute. why?

In the A* algorithm, f(n)=g(n)+h(n), sort the frontier set according to f(n) from small to large. Such a simple addition sometimes results in a large number of [8 queens have been placed, that is g(n)=0, but the logarithm of the queens attacking each other is close to 0, but not 0, that is, the node of h(n)!=0] is placed in the first or first place, and such nodes certainly do not meet the requirements, but Such a node cannot be expanded, because the 8 queens have already been released, and the node can only be skipped and the next node is judged. Such [large amounts], such [sometimes], will sometimes require a large amount of running time, that is, running time is unstable.

The ordering mechanism is modified below.

2.2.3 Minor changes to the A* algorithm code

Program 2: main.py. As the main program, the program 2 code in section 2.2.2 was modified as follows: 1. The code in lines 18 and 19 was deleted because such judgment conditions were no longer needed; 2. The sort conditions in line 27 were modified— —First sort the sequence according to h(n) from small to large; if h(n) is the same, then sort the sequence according to g(n) from small to large. as follows:

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')

The output is as follows:

已找到解序列:[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

The benefits of modifying the frontier sorting mechanism are obvious-the running time is stable within 1s. It can be seen that when using the A* algorithm to solve the problem, whether to simply add g(n) and h(n) to f(n), and then sort according to f(n), is a question worth considering.

Note that the modified code in this section can no longer be calculated as the code of the A* algorithm.


3 reviews

Compared with the GBF algorithm, the A* algorithm code has less running time. After all, the A* algorithm is a global optimization and uses more useful information.


END

Guess you like

Origin blog.csdn.net/qq_40061206/article/details/112033438