八皇后问题,是一个古老而著名的问题,是利用回溯算法求解的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处同一行、同一列或同一斜线上,问有多少种摆法。之后陆续有许多数学家对其进行研究,其中包括高斯和康托,并且将其推广为N皇后问题。
本文利用Python3实现回溯算法,通过遍历搜索得到N皇后问题的所有解,在根据规则删除掉本质相同的解,最后得到该问题的不同的解。此方法相比纯粹的暴力搜索,虽然程序复杂,但是可移植性较高。
下面开始程序的实现,首先定义皇后的个数:
queencount = 8 #可随意设置,如果设置为大于12的数,程序运行较慢。
利用numpy数组的形式代替棋盘,首先获取与已经选择的坐标相冲突的坐标集合,其中冲突包含两种:一是斜线方向上;二是同行同列方向上。然后再得出下一个选择坐标的可行的集合。
# 根据已经选择的坐标点,输出与其冲突的在其斜线方向上的坐标集合
# 为了简化运算,定义一些设置
su = [[i, j] for i in ['+', '-'] for j in ['+', '-']]
sdict = {'+': ' -1'}
# 返回冲突的斜线坐标集合
def simple(zuibiao, sign, sidict=sdict):
save = []
hang = zuibiao[0]
lie = zuibiao[1]
while eval('hang%s' % sidict[sign[0]]) and eval('lie%s' % sidict[sign[1]]):
save.append([hang, lie])
hang = eval('hang %s1' % sign[0])
lie = eval('lie%s1' % sign[1])
return save
# 返回与其冲突的全部坐标 斜线方向+同行同列方向
def allno(func, zuibiao, listsi=su, count=queencount):
alist = []
# 斜线方向
for hs in listsi:
alist += func(zuibiao, hs)# 斜线方向上的冲突坐标集合
# 同行同列方向上的冲突坐标集合
for same in range(0, count):
alist.append([same, zuibiao[1]])
alist.append([zuibiao[0], same])
return alist
# 根据已经选择的坐标,以及冲突的坐标集合,输出摆放第n个皇后的可能的坐标集合
def xikk(newlist, numb=queencount):
fu = []
for tu in newlist:
fu += allno(simple, tu)#所有冲突的坐标集合
slast = []
if len(newlist) >= numb:
return slast
else:
for jgi in [[len(newlist), j] for j in range(0, numb)]: # 必须在第n行选取第n个坐标
if jgi not in fu:
slast.append(jgi)
return slast#返回可能选择的坐标集合
因为采用的是回溯算法搜索,因此这里用字典形式实时记录选择的情况,也就是记录当前选择的是可行坐标集合中的第几个元素。例如:
startdict = {1: 0}#表示的是: 下一次添加的是第一行的可行的坐标集合中的第0个元素
下面给出实现回溯算法函数的程序:
# 回溯算法的函数
def huifu(select, exdict, save, nn=queencount):
#select:已经选择的摆放皇后的坐标集合列表, exdict:存储第几个皇后的可行列表的第几个元素的字典, save:存储皇后问题的解
# 判断当前的状况是不是皇后问题的解
if len(select) == nn:#如果是,则添加到save中
save.append(select)
#求出当前对应select的可行性的坐标集合
fulist = xikk(select)
if len(fulist) != 0:#如果可行性坐标集合不为空
exdict[len(select) + 1] = 0#实时记录下一次选择应该是选择可行性坐标集合中的第几个元素
return huifu(select + [fulist[exdict[len(select)]]], exdict, save)#开始递归
else:
# 如果可行性坐标集合为空
# 开始从后往前计算,找到哪一个可行性坐标集合还有选择的余地
# 如果都已经没有选择了,则遍历完成
for jsa in list(range(1, len(select)))[::-1]:#从后往前计算
fuud = xikk(select[0: jsa])
if exdict[len(select[0: jsa])] < len(fuud) - 1:#说明有选择的余地
# 说明找到一个满足条件的,需要更新字典
newdict = {}
for hh in exdict:
if hh < jsa + 1:
newdict[hh] = exdict[hh]
fur = exdict[jsa] + 1
newdict[jsa] = fur
return huifu(select[0:jsa], newdict, save)#开始递归
return save
下面给出棋盘的第一行中,每一个位置上如果有皇后时得到的所有解,然后再相加起来,从而得到所有的解。
#获得所有解
sult = []
for jj in [[0, dfu] for dfu in range(0, queencount)]:#遍历第0行的元素,找到每一个位置有皇后时的所有解
sult = huifu([jj], startdict, sult)#相加
#下面给出8皇后问题的部分解:
[[0, 0], [1, 4], [2, 7], [3, 5], [4, 2], [5, 6], [6, 1], [7, 3]]
[[0, 0], [1, 5], [2, 7], [3, 2], [4, 6], [5, 3], [6, 1], [7, 4]]
[[0, 0], [1, 6], [2, 3], [3, 5], [4, 7], [5, 1], [6, 4], [7, 2]]
[[0, 0], [1, 6], [2, 4], [3, 7], [4, 1], [5, 3], [6, 5], [7, 2]]
[[0, 1], [1, 3], [2, 5], [3, 7], [4, 2], [5, 0], [6, 6], [7, 4]]
[[0, 1], [1, 4], [2, 6], [3, 0], [4, 2], [5, 7], [6, 5], [7, 3]]
[[0, 1], [1, 4], [2, 6], [3, 3], [4, 0], [5, 7], [6, 5], [7, 2]]
[[0, 1], [1, 5], [2, 0], [3, 6], [4, 3], [5, 7], [6, 2], [7, 4]]
[[0, 1], [1, 5], [2, 7], [3, 2], [4, 0], [5, 3], [6, 6], [7, 4]]
[[0, 1], [1, 6], [2, 2], [3, 5], [4, 7], [5, 4], [6, 0], [7, 3]]
[[0, 1], [1, 6], [2, 4], [3, 7], [4, 0], [5, 3], [6, 5], [7, 2]]
[[0, 1], [1, 7], [2, 5], [3, 0], [4, 2], [5, 4], [6, 6], [7, 3]]
[[0, 2], [1, 0], [2, 6], [3, 4], [4, 7], [5, 1], [6, 3], [7, 5]]
[[0, 2], [1, 4], [2, 1], [3, 7], [4, 0], [5, 6], [6, 3], [7, 5]]
[[0, 2], [1, 4], [2, 1], [3, 7], [4, 5], [5, 3], [6, 6], [7, 0]]
下面是对本质相同的解的说明:如果解A通过主、副对角线对称转换,或者通过旋转90度的倍数的转换, 或者经过以上的组合转换可以得到解B,,则认为解A与解B是本质一样的解。
下面给出关于副对角线对称解的说明:求解A关于副对角线对称的解A副,就相当于先将A顺时针移动90度得到A1,然后求A1关于主对角线对称的解A1主,也就是A副=A1主。详细展示看下图:
下面给出删除本质一样的解的程序:
import numpy as np#引入numpy数组库
#通过主对角线对称变换[也就是转置]可以得到的解
def trans(exlist):
yuannp = np.zeros((len(exlist), len(exlist)))
for jj in exlist:
yuannp[jj[0], jj[1]] = 1
#获得所有的同质解
#获得转置
trannp = yuannp.T
# 开始获得值为1 的坐标
si = []
for ihang in range(0, len(exlist)):
for jlie in range(0, len(exlist)):
if trannp[ihang][jlie] == 1:
si.append([ihang, jlie])
return si
#顺时针每旋转90度产生一个本质一样的解,以及关于主、副对角线对称的解
def sameslover(siglelist):
yuannp = np.zeros((len(siglelist), len(siglelist)))#构建全0数组
for jj in siglelist:
yuannp[jj[0], jj[1]] = 1#选中的坐标,其值为1
#开始旋转3次,因为旋转4次,与开始的重合,所以旋转3次
xuanzhuan = 0
#获得所有的同质解
tongzhi = []
while xuanzhuan < 3:
tongzhi.append(trans(siglelist))#添加 主对角线 对称的解
xuanzhuan += 1
#开始向右翻转90度,形成的新数组的规则为:新数组的行从左到右,是旧数组的列从下往上依次组成的
newnp = []
for lienu in range(0, len(siglelist)):
lie = []
for low_up in range(len(siglelist)-1, -1, -1):
lie.append(yuannp[low_up, lienu])
newnp.append(lie)
#开始获得值为1 的坐标
si = []
for ihang in range(0, len(siglelist)):
for jlie in range(0, len(siglelist)):
if newnp[ihang][jlie] == 1:
si.append([ihang, jlie])
tongzhi.append(si)
siglelist = si.copy()#通过这种变换,来获得关于 副对角线 对称的解
#在此基础上继续翻转
yuannp = np.array(newnp).copy()
tongzhi.append(trans(siglelist))#因为需要四个主对角线,循环里面有3次,因此需要加上最后一次,也就是旋转了270度形成的解 的主对角线 对称的解
return tongzhi
print(len(sult))
#开始去除本质相同的解
for jj in sult:
for gggg in sameslover(jj):
if gggg in sult and gggg != jj:#不能删除和初始解一样的解
sult.remove(gggg)
print(len(sult)
下面给出从1-12个皇后问题的相关解数的结果:
重点说明:一般来说,Python和Java,C#一样,并没有尾递归自动优化的能力,递归调用会受到调用栈长度的限制。因此当设置皇后的数量大于9时,程序会崩溃。解决办法有很多种,其中一种比较笨但容易实现的方法参见,本文采取的尾递归优化方法参见。为了适应python3,对原程序进行了适当的小调整。
#尾递归优化
import sys
class TailRecurseException(BaseException):
def __init__(self, args, kwargs):
self.args = args
self.kwargs = kwargs
def tail_call_optimized(g):
def func(*args, **kwargs):
f = sys._getframe()
if f.f_back and f.f_back.f_back and f.f_back.f_back.f_code == f.f_code:
raise TailRecurseException(args, kwargs)
else:
while 1:
try:
return g(*args, **kwargs)
except TailRecurseException as e:
args = e.args
kwargs = e.kwargs
func.__doc__ = g.__doc__
return func
在使用了递归的函数前,也就是本文中的回溯算法函数huifu前添加装饰器语句@tail_call_optimized即可。如此设置之后,当皇后的个数大于9时,程序依然运行良好,因为是遍历搜索,运行可能较慢。
为了更好的展示皇后问题的解,本文依然采用图片合成gif的形式,具体实现方法参见如下程序:
#开始绘图展示
from pylab import mpl # 作图显示中文
mpl.rcParams['font.sans-serif'] = ['FangSong'] # 设置中文字体为新宋体
import matplotlib.pyplot as plt
#下图中以黑色方块代表皇后
scount = 1
for i_so in sult:
#开始绘制棋盘
for i in range(0, len(i_so) + 1):
plt.axhline(y = i, linewidth = 3, color = 'k')
plt.axvline(x = i, linewidth = 3, color = 'k')
plt.axis([0, len(i_so), 0, len(i_so)])
plt.title('%d皇后问题不同解: 第%s个解' %(queencount, scount))
#开始绘制皇后, 黑色方块代表皇后
for jif in i_so:
plt.broken_barh([(jif[1], 1)], (len(i_so)-1- jif[0], 1), facecolors='k')
plt.axis('off')#关闭坐标轴
plt.savefig(r"C:\Users\GWT9\Desktop\queen\%s_%s.png" %(queencount, scount))#图片保存
plt.close()
scount += 1
#最终的图片合成gif
import os
os.chdir(r'C:\Users\GWT9\Desktop\queen')#设置当前工作目录
import imageio# 引入合成gif的库
#所有的图片name集合
namelist = []
for jjj in range(1, len(sult) + 1):
namelist += ['%s_%s.png'%(queencount, jjj)] * 8 #数字8控制动态图中每类图片的显示时间
#创建gif的函数
def create_gif(image_list, gif_name):
frames = []
for image_name in image_list:
frames.append(imageio.imread(image_name))
imageio.mimsave(gif_name, frames, 'GIF', duration=0.2)#duration控制动态图中每张图片的显示时间
return
create_gif(namelist + [namelist[-1]] * 8, 'queen.gif') #数字8控制动态图中每类图片的显示时间
8皇后问题的不同的12个解的展示动态图片见文章开头。