有一个8乘8的棋盘,现在要将八个皇后放到棋盘上,满足:对于每一个皇后,在自己所在的行、列、两个对角线都没有其他皇后。
规定初始状态为【空棋盘】,动作为【每次只在最左面未放置皇后的列上放一个皇后】。这样就使得棋盘的同列最多只能出现一个皇后。
不了解DFS/BFS/UCS的话,请看这里。
程序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个函数以供主程序调用。
深度优先搜索(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
宽度优先搜索(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:main.py。为主程序,通过调用程序1的二个函数,完成UCS解决八皇后问题的全过程。如下:
import random
import time
from functions import attacked_queens_pairs, display_board
start = time.time()
frontier_priority_queue = [{'placed_queens':0, 'seqs':[0] * 8}] # 使用优先级队列去存储未扩展的叶子节点;初始状态为8个0,代表棋盘上无皇后;g(n)=已放置好的皇后个数,初始g(n)=0
solution = []
flag = 0 # 代表还未找到解
while frontier_priority_queue: # 若frontier非空就继续循环,若成功找到解则跳出循环输出解,若frontier为空时还未找到解则宣告失败
first = frontier_priority_queue.pop(0) # 先扩展g(n)最大的序列;由于每次都会按g(n)将各个序列从大到小排序,所以扩展第一个序列
seqs = first['seqs']
if first['placed_queens'] == 8: # 扩展节点前做goal test:若八个皇后均已放好,且g(n)=8时,序列为解序列
solution = seqs
flag = 1 # 成功
break
nums = list(range(1, 9)) # 元素为1-8的列表
pos = seqs.index(0)
for j in range(8): # 在序列中第一个为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({'placed_queens':8-temp_seqs.count(0), 'seqs':temp_seqs}) # frontier中节点的结构包括:已放置好的皇后个数、节点对应的序列
frontier_priority_queue = sorted(frontier_priority_queue, key=lambda x:x['placed_queens'], reverse=True) # 将序列按key=placed_queens从大到小排序,因为已放置好的皇后个数越多,意味着越接近目标状态
if solution:
print('已找到解序列:' + str(solution))
display_board(solution)
else:
print('算法失败,未找到解')
end = time.time()
print('用时' + str('%.2f' % (end-start)) + 's')
一种输出如下:
已找到解序列:[4, 6, 8, 2, 7, 1, 3, 5]
对应棋盘如下:
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 0 0 0 0 0 1
0 1 0 0 0 0 0 0
0 0 0 0 1 0 0 0
0 0 1 0 0 0 0 0
攻击的皇后对数为0
用时0.01s
上述代码将UCS的g(n)设置为已放置好的皇后个数,而且每次都会将frontier集按g(n)将各个序列从大到小排序。
程序2:main.py。为主程序,将【2.2.3 UCS(一致代价搜索)】的程序2代码修改了下:1、将第26行代码中排序时的【reverse=True】去掉;2、在最后几行添加了输出frontier元素和长度的代码。如下:
import random
import time
from functions import attacked_queens_pairs, display_board
start = time.time()
frontier_priority_queue = [{'placed_queens':0, 'seqs':[0] * 8}] # 使用优先级队列去存储未扩展的叶子节点;初始状态为8个0,代表棋盘上无皇后;g(n)=已放置好的皇后个数,初始g(n)=0
solution = []
flag = 0 # 代表还未找到解
while frontier_priority_queue: # 若frontier非空就继续循环,若成功找到解则跳出循环输出解,若frontier为空时还未找到解则宣告失败
first = frontier_priority_queue.pop(0) # 先扩展g(n)最大的序列;由于每次都会按g(n)将各个序列从大到小排序,所以扩展第一个序列
seqs = first['seqs']
if first['placed_queens'] == 8: # 扩展节点前做goal test:若八个皇后均已放好,且g(n)=8时,序列为解序列
solution = seqs
flag = 1 # 成功
break
nums = list(range(1, 9)) # 元素为1-8的列表
pos = seqs.index(0)
for j in range(8): # 在序列中第一个为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({'placed_queens':8-temp_seqs.count(0), 'seqs':temp_seqs}) # frontier中节点的结构包括:已放置好的皇后个数、节点对应的序列
frontier_priority_queue = sorted(frontier_priority_queue, key=lambda x:x['placed_queens']) # 将序列按key=placed_queens从大到小排序,因为已放置好的皇后个数越多,意味着越接近目标状态
if solution:
print('已找到解序列:' + str(solution))
display_board(solution)
else:
print('算法失败,未找到解')
end = time.time()
print('用时' + str('%.2f' % (end-start)) + 's')
for i in frontier_priority_queue:
print(i)
print(len(frontier_priority_queue))
输出为:
已找到解序列:[4, 1, 5, 8, 2, 7, 3, 6]
对应棋盘如下:
0 1 0 0 0 0 0 0
0 0 0 0 1 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 0 0 0 1
0 0 0 0 0 1 0 0
0 0 0 1 0 0 0 0
攻击的皇后对数为0
用时1.59s
{'placed_queens': 8, 'seqs': [4, 1, 5, 8, 6, 3, 7, 2]}
{'placed_queens': 8, 'seqs': [4, 8, 1, 5, 7, 2, 6, 3]}
{'placed_queens': 8, 'seqs': [4, 8, 1, 3, 6, 2, 7, 5]}
{'placed_queens': 8, 'seqs': [4, 8, 5, 3, 1, 7, 2, 6]}
{'placed_queens': 8, 'seqs': [4, 2, 8, 5, 7, 1, 3, 6]}
{'placed_queens': 8, 'seqs': [4, 2, 8, 6, 1, 3, 5, 7]}
{'placed_queens': 8, 'seqs': [4, 2, 7, 5, 1, 8, 6, 3]}
{'placed_queens': 8, 'seqs': [4, 2, 7, 3, 6, 8, 1, 5]}
{'placed_queens': 8, 'seqs': [4, 2, 7, 3, 6, 8, 5, 1]}
{'placed_queens': 8, 'seqs': [4, 2, 5, 8, 6, 1, 3, 7]}
{'placed_queens': 8, 'seqs': [4, 7, 3, 8, 2, 5, 1, 6]}
{'placed_queens': 8, 'seqs': [4, 7, 5, 3, 1, 6, 8, 2]}
{'placed_queens': 8, 'seqs': [4, 7, 5, 2, 6, 1, 3, 8]}
{'placed_queens': 8, 'seqs': [4, 7, 1, 8, 5, 2, 6, 3]}
{'placed_queens': 8, 'seqs': [4, 6, 8, 3, 1, 7, 5, 2]}
{'placed_queens': 8, 'seqs': [4, 6, 8, 2, 7, 1, 3, 5]}
{'placed_queens': 8, 'seqs': [4, 6, 1, 5, 2, 8, 3, 7]}
{'placed_queens': 8, 'seqs': [8, 2, 5, 3, 1, 7, 4, 6]}
{'placed_queens': 8, 'seqs': [8, 2, 4, 1, 7, 5, 3, 6]}
{'placed_queens': 8, 'seqs': [8, 4, 1, 3, 6, 2, 7, 5]}
{'placed_queens': 8, 'seqs': [8, 3, 1, 6, 2, 5, 7, 4]}
{'placed_queens': 8, 'seqs': [1, 7, 4, 6, 8, 2, 5, 3]}
{'placed_queens': 8, 'seqs': [1, 7, 5, 8, 2, 4, 6, 3]}
{'placed_queens': 8, 'seqs': [1, 5, 8, 6, 3, 7, 2, 4]}
{'placed_queens': 8, 'seqs': [1, 6, 8, 3, 7, 4, 2, 5]}
{'placed_queens': 8, 'seqs': [3, 5, 8, 4, 1, 7, 2, 6]}
{'placed_queens': 8, 'seqs': [3, 5, 2, 8, 6, 4, 7, 1]}
{'placed_queens': 8, 'seqs': [3, 5, 2, 8, 1, 7, 4, 6]}
{'placed_queens': 8, 'seqs': [3, 5, 7, 1, 4, 2, 8, 6]}
{'placed_queens': 8, 'seqs': [3, 7, 2, 8, 5, 1, 4, 6]}
{'placed_queens': 8, 'seqs': [3, 7, 2, 8, 6, 4, 1, 5]}
{'placed_queens': 8, 'seqs': [3, 6, 2, 5, 8, 1, 7, 4]}
{'placed_queens': 8, 'seqs': [3, 6, 2, 7, 1, 4, 8, 5]}
{'placed_queens': 8, 'seqs': [3, 6, 2, 7, 5, 1, 8, 4]}
{'placed_queens': 8, 'seqs': [3, 6, 4, 2, 8, 5, 7, 1]}
{'placed_queens': 8, 'seqs': [3, 6, 4, 1, 8, 5, 7, 2]}
{'placed_queens': 8, 'seqs': [3, 6, 8, 1, 5, 7, 2, 4]}
{'placed_queens': 8, 'seqs': [3, 6, 8, 1, 4, 7, 5, 2]}
{'placed_queens': 8, 'seqs': [3, 6, 8, 2, 4, 1, 7, 5]}
{'placed_queens': 8, 'seqs': [3, 1, 7, 5, 8, 2, 4, 6]}
{'placed_queens': 8, 'seqs': [3, 8, 4, 7, 1, 6, 2, 5]}
{'placed_queens': 8, 'seqs': [5, 8, 4, 1, 3, 6, 2, 7]}
{'placed_queens': 8, 'seqs': [5, 8, 4, 1, 7, 2, 6, 3]}
{'placed_queens': 8, 'seqs': [5, 7, 4, 1, 3, 8, 6, 2]}
{'placed_queens': 8, 'seqs': [5, 7, 2, 6, 3, 1, 4, 8]}
{'placed_queens': 8, 'seqs': [5, 7, 2, 6, 3, 1, 8, 4]}
{'placed_queens': 8, 'seqs': [5, 7, 2, 4, 8, 1, 3, 6]}
{'placed_queens': 8, 'seqs': [5, 7, 1, 3, 8, 6, 4, 2]}
{'placed_queens': 8, 'seqs': [5, 7, 1, 4, 2, 8, 6, 3]}
{'placed_queens': 8, 'seqs': [5, 1, 4, 6, 8, 2, 7, 3]}
{'placed_queens': 8, 'seqs': [5, 1, 8, 6, 3, 7, 2, 4]}
{'placed_queens': 8, 'seqs': [5, 1, 8, 4, 2, 7, 3, 6]}
{'placed_queens': 8, 'seqs': [5, 2, 8, 1, 4, 7, 3, 6]}
{'placed_queens': 8, 'seqs': [5, 2, 6, 1, 7, 4, 8, 3]}
{'placed_queens': 8, 'seqs': [5, 2, 4, 6, 8, 3, 1, 7]}
{'placed_queens': 8, 'seqs': [5, 2, 4, 7, 3, 8, 6, 1]}
{'placed_queens': 8, 'seqs': [5, 3, 1, 7, 2, 8, 6, 4]}
{'placed_queens': 8, 'seqs': [5, 3, 1, 6, 8, 2, 4, 7]}
{'placed_queens': 8, 'seqs': [5, 3, 8, 4, 7, 1, 6, 2]}
{'placed_queens': 8, 'seqs': [7, 4, 2, 8, 6, 1, 3, 5]}
{'placed_queens': 8, 'seqs': [7, 4, 2, 5, 8, 1, 3, 6]}
{'placed_queens': 8, 'seqs': [7, 5, 3, 1, 6, 8, 2, 4]}
{'placed_queens': 8, 'seqs': [7, 3, 8, 2, 5, 1, 6, 4]}
{'placed_queens': 8, 'seqs': [7, 3, 1, 6, 8, 5, 2, 4]}
{'placed_queens': 8, 'seqs': [7, 2, 6, 3, 1, 4, 8, 5]}
{'placed_queens': 8, 'seqs': [7, 2, 4, 1, 8, 5, 3, 6]}
{'placed_queens': 8, 'seqs': [7, 1, 3, 8, 6, 4, 2, 5]}
{'placed_queens': 8, 'seqs': [2, 4, 6, 8, 3, 1, 7, 5]}
{'placed_queens': 8, 'seqs': [2, 6, 8, 3, 1, 4, 7, 5]}
{'placed_queens': 8, 'seqs': [2, 6, 1, 7, 4, 8, 3, 5]}
{'placed_queens': 8, 'seqs': [2, 8, 6, 1, 3, 5, 7, 4]}
{'placed_queens': 8, 'seqs': [2, 7, 3, 6, 8, 5, 1, 4]}
{'placed_queens': 8, 'seqs': [2, 7, 5, 8, 1, 4, 6, 3]}
{'placed_queens': 8, 'seqs': [2, 5, 7, 4, 1, 8, 6, 3]}
{'placed_queens': 8, 'seqs': [2, 5, 7, 1, 3, 8, 6, 4]}
{'placed_queens': 8, 'seqs': [6, 8, 2, 4, 1, 7, 5, 3]}
{'placed_queens': 8, 'seqs': [6, 4, 2, 8, 5, 7, 1, 3]}
{'placed_queens': 8, 'seqs': [6, 4, 7, 1, 8, 2, 5, 3]}
{'placed_queens': 8, 'seqs': [6, 4, 7, 1, 3, 5, 2, 8]}
{'placed_queens': 8, 'seqs': [6, 4, 1, 5, 8, 2, 7, 3]}
{'placed_queens': 8, 'seqs': [6, 2, 7, 1, 3, 5, 8, 4]}
{'placed_queens': 8, 'seqs': [6, 2, 7, 1, 4, 8, 5, 3]}
{'placed_queens': 8, 'seqs': [6, 1, 5, 2, 8, 3, 7, 4]}
{'placed_queens': 8, 'seqs': [6, 3, 1, 7, 5, 8, 2, 4]}
{'placed_queens': 8, 'seqs': [6, 3, 1, 8, 5, 2, 4, 7]}
{'placed_queens': 8, 'seqs': [6, 3, 1, 8, 4, 2, 7, 5]}
{'placed_queens': 8, 'seqs': [6, 3, 7, 4, 1, 8, 2, 5]}
{'placed_queens': 8, 'seqs': [6, 3, 7, 2, 4, 8, 1, 5]}
{'placed_queens': 8, 'seqs': [6, 3, 7, 2, 8, 5, 1, 4]}
{'placed_queens': 8, 'seqs': [6, 3, 5, 8, 1, 4, 2, 7]}
{'placed_queens': 8, 'seqs': [6, 3, 5, 7, 1, 4, 2, 8]}
91
发现运行时间变长了,这是由于每次将frontier的各个序列按g(n)从小到大排序,而这意味着后面的序列才是最接近目标状态的,但是我这样做是有原因的——会发现frontier_priority_queue中有91个序列,且均是符合条件的解。根据自己写的暴力破解程序,解共有92个,这里输出91个是因为在此之前pop了第一个序列,因此少了一个。能求出所有解,也算是自己在本节写的修改的UCS代码的一个优点吧。
UCS算法在复杂问题上虽然总可以找到最优解,但是有时候走的路径比较绕远,即时间复杂度与空间复杂度上表现较差。
因为DFS是先扩展最新加入frontier集的那个节点(LIFO),因此很快就可以将八个皇后放在合适的位置;UCS代码运行时间也很快;修改后的UCS代码可以在frontier集中记录所有合适的解;BFS的运行时间最长,只因BFS是扩展最浅层的节点。
因此,根据三种算法的平均运行时间,DFS、UCS解决八皇后问题的效率与性能较高,BFS最差。
END