Python3 趣味系列题5 ------回溯+尾递归优化解决N皇后问题

queen.gif

  八皇后问题,是一个古老而著名的问题,是利用回溯算法求解的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处同一行、同一列或同一斜线上,问有多少种摆法。之后陆续有许多数学家对其进行研究,其中包括高斯和康托,并且将其推广为N皇后问题
  本文利用Python3实现回溯算法,通过遍历搜索得到N皇后问题的所有解,在根据规则删除掉本质相同的解,最后得到该问题的不同的解。此方法相比纯粹的暴力搜索,虽然程序复杂,但是可移植性较高。
  下面开始程序的实现,首先定义皇后的个数:

queencount = 8 #可随意设置,如果设置为大于12的数,程序运行较慢。

  利用numpy数组的形式代替棋盘,首先获取与已经选择的坐标相冲突的坐标集合,其中冲突包含两种:一是斜线方向上;二是同行同列方向上。然后再得出下一个选择坐标的可行的集合。

# 根据已经选择的坐标点,输出与其冲突的在其斜线方向上的坐标集合
# 为了简化运算,定义一些设置
su = [[i, j] for i in ['+', '-'] for j in ['+', '-']]
sdict = {'+': '<queencount', '-': '> -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主。详细展示看下图:

slover.png

  下面给出删除本质一样的解的程序:

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个皇后问题的相关解数的结果:

queen.png

  重点说明:一般来说,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个解的展示动态图片见文章开头。

欢迎讨论与关注!!!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容