算法之八皇后问题详解暨终极极限挑战

       八皇后问题是一个以国际象棋为背景的问题:如何能够在 8×8 的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行、纵行或斜线上。八皇后问题可以推广为更一般的n皇后摆放问题:这时棋盘的大小变为n1×n1,而皇后个数也变成n2。而且仅当 n2 = 1 或 n1 ≥ 4 时问题有解。

八皇后问题最早是由国际西洋棋棋手马克斯·贝瑟尔于1848年提出。之后陆续有数学家对其进行研究,其中包括高斯和康托,并且将其推广为更一般的n皇后摆放问题。八皇后问题的第一个解是在1850年由弗朗兹·诺克给出的。诺克也是首先将问题推广到更一般的n皇后摆放问题的人之一。1874年,S.冈德尔提出了一个通过行列式来求解的方法,这个方法后来又被J.W.L.格莱舍加以改进。

一、暴力求解

设八个皇后为xi,分别在第i行(i=1,2,3,4……,8);

问题的解状态:可以用(1,x1),(2,x2),……,(8,x8)表示8个皇后的位置;

由于行号固定,可简单记为:(x1,x2,x3,x4,x5,x6,x7,x8);

问题的解空间:(x1,x2,x3,x4,x5,x6,x7,x8),1≤xi≤8(i=1,2,3,4……,8),共8的8次方个状态;

约束条件:八个(1,x1),(2,x2) ,(3,x3),(4,x4) ,(5,x5), (6,x6) , (7,x7), (8,x8)不在同一行、同一列和同一对角线上。

盲目的枚举算法:通过8重循环模拟搜索空间中的88个状态,从中找出满足约束条件的“答案状态”。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
八皇后——盲目迭代法
"""
def check_1(a, n):
    for i in range(1, n):
        for j in range(0, i):
            if a[i] == a[j] or abs(a[i]-a[j]) == i - j:
                return False
    return True # 不冲突

def queens_1():
    a= [0 for i in range(8)]
    count = 0
    for a[0] in range(8):
        for a[1] in range(8):
            for a[2] in range(8):
                for a[3] in range(8):
                    for a[4] in range(8):
                        for a[5] in range(8):
                            for a[6] in range(8):
                                for a[7] in range(8):
                                    if check_1(a,8) == False:
                                        continue
                                    else:
                                        print(a)
                                        count +=1
    print('八皇后问题的全部解法:',count)

if __name__ == '__main__':
    queens_1()

以上的思路很简单,但是效率不高,时间复杂度为N的N次方,八皇后会跑到45s左右,代码不好看,而且只能求八皇后问题,无法扩展到n皇后,所以接下来用其他思路

其实在上面穷举的过程中有些情况是不需要再尝试的,但是我们没有及时收敛,下面的算法均是对穷举的优化。

二、回溯法

  回溯的基本思想:从问题的某一种状态出发,搜索可以到达的所有状态。当某个状态到达后,可向前回退,并继续搜索其他可达状态。当所有状态都到达后,回溯算法结束!

这个算法就是一个搜索算法,对一棵树进行深度优先搜索,但是在搜索的过程中具有自动终止返回上一层继续往下搜索的能力,这个算法其实就是一个搜索树,对部分节点进行了剪枝是一种可行的算法,对八皇后这样皇后数较少的问题还能够解决,如果皇后数再大一点就无能为力了

如图,说明八皇后问题中的回溯算法:

      

注意:其实就是不断的通过递归函数,去往棋盘中尝试放皇后,成功就继续递归(即继续放皇后),失败就跳出递归函数,回溯到上层递归函数中,上层递归函数中保存着上一个皇后的位置!!!这就是八皇后中,回溯的概念!

算法思考,初步思路:

首先如何决定下一个皇后能不能放这里可以有两种思路,

第一种是尝试维护一个8*8的二维矩阵,

每次找到一个空位放下一个皇后就把对应行列对角线上的棋格做个标记,摆放后立即调用一个验证函数(传递整个棋盘的数据),验证合理性,安全则摆放下一个,不安全则尝试摆放这一行的下一个位置,直至摆到棋盘边界,当这一行所有位置都无法保证皇后安全时,需要回退到上一行,清除上一行的摆放记录,并且在上一行尝试摆放下一位置的皇后(回溯算法的核心)当摆放到最后一行,并且调用验证函数确定安全后,累积数自增1,表示有一个解成功算出.验证函数中,需要扫描当前摆放皇后的左上,中上,右上方向是否有其他皇后,有的话存在危险,没有则表示安全,并不需要考虑当前位置棋盘下方的安全性,因为下面的皇后还没有摆放

回溯算法的天然实现是使用编译器的递归函数,但是其性能令人遗憾

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
维护8×8格的矩阵,递归求解
"""
import time
def QueenAtRow(chess, row):
    num = len(chess[0])
    if row == num:
        print("---"* num)
        for i in range(num):
            print(chess[i], ' ')
        return
    chessTmp = chess
    # 向这一行的每一个位置尝试排放皇后
    # 然后检测状态,如果安全则继续执行递归函数摆放下一行皇后
    for i in range(num):
        for j in range(num):
            # 摆放这一行的皇后,之前要清掉所有这一行摆放的记录
            chessTmp[row][j] = 0
        chessTmp[row][i] = 1
        if is_conflict(chessTmp, row ,i):
            QueenAtRow(chessTmp,row +1)

def is_conflict(chess,row ,col):
    step = 1
    while row - step >= 0:
        if chess[row - step][col] == 1:
            return False
        if col - step >= 0 and chess[row - step][col - step] == 1:
            return False
        if col + step < len(chess[0]) and chess[row - step][col + step] ==1:
            return False
        step +=1
    return True

if __name__ == '__main__':
    queenNum = 8   #修改不同的皇后数
    chess = [[0 for i in range(queenNum)] for j in range(queenNum)] #初始化棋盘,全部置0
    time1 = time.time()
    QueenAtRow(chess, 0)
    print('消耗时间为:', time.time() - time1)

时间八皇后跑到0.05s,用上面的代码尝试算9-16皇后问题,开始尝试解决16皇后问题时,发现时间复杂度已经超出我的心里预期,一部分是由于代码会输出排列出的结果IO导致,另一方面确实耗时很长,跑了两小时都没跑完。

目前N皇后的国际记录,已经有科研单位给出了25皇后的计算结果,耗时暂未可知。一些算法高手能在100秒内跑16皇后,上面的算法效率只能说一般,仍然可以优化:

第二种方法通过一维数组维护记录每个皇后的位置

放弃构造矩阵来模拟棋盘位置,我们把问题更加抽象化,八个皇后能放下一定是一行放一个,只需一个数组记录每个皇后的列数(默认第N个放第N行),那么问题就被抽象成了数组的第N个数和前N-1个数不存在几个和差关系即可(比如差不为零代表不在同一列)。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
维护一维数组,递归求解
"""
import time
def QueenAtRow2(chess, row):
    num = len(chess)
    if row == num:
        print("---" * num)
        print(chess, ' ')
        return
    chessTmp = chess
    # 向这一行的每一个位置尝试排放皇后
    # 然后检测状态,如果安全则继续执行递归函数摆放下一行皇后
    for i in range(num):
        # 摆放这一行的皇后,之前要清掉所有这一行摆放的记录
        chessTmp[row] = i
        if is_conflict(chessTmp, row, i):
            QueenAtRow2(chessTmp, row +1)

def is_conflict(chess,row ,col):
    step = 1
    # 判断中上、左上、右上是否冲突
    for i in range(row-1, -1, -1):
        if chess[i] == col:
            return False
        if chess[i] == col - step:
            return False
        if chess[i] == col + step:
            return False
        step +=1
    return True

if __name__ == '__main__':
    for queenNum in range(4,10):
        # queenNum = 8  # 修改不同的皇后数
        chess = [0 for j in range(queenNum)]  # 初始化棋盘,全部置0
        time1 = time.time()
        QueenAtRow2(chess, 0)
        print('消耗时间为:', time.time() - time1)

与原来相比这样耗时减少了一半,有了一定的提升。

第三种思路手动维护一个栈

由于递归可以看做底层帮你维护的一个堆栈不断地push、pop,我们也可以通过手动维护一个堆栈来模拟这个递归调用的过程,只要构造两个函数backward(往后回溯)、refresh(向前刷新)来模拟堆栈进出即可。

时间复杂度是一样的,空间复杂度因为自定义了class,有所上升。很可惜经过测试其性能没有得到提升。

https://blog.csdn.net/codes_first/article/details/78474226

此外这里还有通过对称剪枝”,“递归回溯”,“多线程”优化后的算法实现

https://www.hexcode.cn/article/show/eight-queen

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
八皇后手动维护堆栈python解法
"""
import queue
def EightQueen(board):
    blen = len(board)
    stack = queue.LifoQueue()  # 用后进先出队列来模拟一个栈
    stack.put((0,0))    # 为了自洽的初始化
    while not stack.empty():
        i,j = stack.get()
        if check(board,i,j):    # 当检查通过
            board[i] = j    # 别忘了放Queen
            if i >= blen - 1:
                print(board)   # i到达最后一行表明已经有了结果
                printBoard(board)
                # break
            else:
                if j < blen - 1:    # 虽然说要把本位置右边一格入栈,但是如果本位置已经是行末尾,那就没必要了
                    stack.put((i,j+1))
                stack.put((i+1,0))    # 下一行第一个位置入栈,准备开始下一行的扫描
        elif j < blen - 1:
            stack.put((i,j+1))    # 对于未通过检验的情况,自然右移一格即可

def check(board,row,col):
    i = 0
    while i < row:
        if abs(col-board[i]) in (0,abs(row-i)):
            return False
        i += 1
    return True

def printBoard(board):
    '''为了更友好地展示结果 方便观察'''
    import sys
    for i,col in enumerate(board):
        sys.stdout.write('□ ' * col + '■ ' + '□ ' * (len(board) - 1 - col))
        print(' ')

if __name__ == '__main__':
    queenNum = 8
    board = [0 for i in range(queenNum)]
    EightQueen(board)

第四种全排列思路

由于八个皇后的任意两个不能处在同一行,那么这肯定是每一个皇后占据一行。于是我们可以定义一个数组ColumnIndex[8],数组中第i个数字表示位于第i行的皇后的列号。先把ColumnIndex的八个数字分别用0-7初始化,接下来我们要做的事情就是对数组ColumnIndex做全排列。由于我们是用不同的数字初始化数组中的数字,因此任意两个皇后肯定不同列。我们只需要判断得到的每一个排列对应的八个皇后是不是在同一对角斜线上,也就是数组的两个下标i和j,是不是i-j==ColumnIndex[i]-ColumnIndex[j]或者j-i==ColumnIndex[i]-ColumnIndex[j]。解空间降低到8 的阶乘

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
全排列解八皇后问题'2018/9/12'
"""
from copy import deepcopy
def EightQueenByFP(src, slen):
    # 后找:字符串中最后一个升序的位置i,即:S[i]<S[i+1]
    i = slen - 2
    while i >= 0 and src[i] >= src[i+1]:
        i -=1
    if i < 0:
        return False
    # 查找(小大):S[i+1…N-1]中比S[i]大的最小值S[j]
    j = slen -1
    while src[j] <= src[i]:
        j -=1
    # 交换:S[i],S[j]
    src[j], src[i] = src[i], src[j]
    # 翻转:S[i+1…N-1]
    Reverse(src, i+1, slen-1)
    return True

def swap(li, i, j):
    if i == j:
        return
    temp = li[j]
    li[j] = li[i]
    li[i] = temp
def is_conflict(src):
    slen = len(src)
    for i in range(slen):
        for j in range(i+1,slen):
            if src[i] - src[j] == i - j or src[j] - src[i] == i - j:
                return False
                break
    return True

def Reverse(li, i, j):
    if li is None or i < 0 or j < 0 or i >= j or len(li) < j + 1:
        return
    while i < j:
        swap(li, i, j)
        i += 1
        j -= 1

if __name__ == '__main__':
    queenNum = 8
    src = [i for i in range(queenNum)]
    result = [deepcopy(src)]
    count = 0
    while EightQueenByFP(src, len(src)):
        if is_conflict(src):  # 若对角线冲突则不满足放置条件,没有冲突为True
            result.append(deepcopy(src))
            count +=1
    for i in result:
        print(i)
    print(queenNum, '皇后问题的全部解法:',count)

第五种python花式求解实现

#!/usr/bin/env python
# _*_ coding:utf-8 _*_
'''
回溯法解N皇后问题
'''
import random
#冲突检查,在定义state时,采用state来标志每个皇后的位置,
# 其中索引用来表示横坐标,其对应的值表示纵坐标,
# 例如: state[0]=3,表示该皇后位于第1行的第4列上
def is_conflict(state,nextX):
    nextY = len(state)
    for i in range(nextY):
        # 如果下一个皇后的位置与当前的皇后位置相邻(包括上下,左右)或在同一对角线上,则说明有冲突,需要重新摆放
        if abs(state[i] - nextX) in (0, nextY - i):
            return True
    return False

#采用生成器的方式来产生每一个皇后的位置,并用递归来实现下一个皇后的位置。
def Queens(num, state= ()):
    # 这个state=()说明state一上来是一个空元组
    count = 0
    for pos in range(num):
        if not is_conflict(state, pos):
            # 产生当前皇后的位置信息
            if len(state) == num - 1:
                yield (pos,)
            # 否则,把当前皇后的位置信息,添加到状态列表里,并传递给下一皇后。
            else:
                for result in Queens(num, state + (pos,)):
                    yield (pos,) + result

#为了直观表现棋盘,用X表示每个皇后的位置
def prettyprint(solution):
    def line(pos, length=len(solution)):
        return '. ' * (pos) + 'X ' + '. '*(length-pos-1)
    for pos in solution:
        print( line(pos))

if __name__ == '__main__':
    qNum = 8
    res = list(Queens(qNum))
    for i in res:
        print(i)
        # prettyprint(i)
    print(len(res))
    # prettyprint(random.choice(res))

除了上面介绍的这些,最主要的是递归回溯的思路,此外还有通过位运算的方法,以及网上还有写的更简练的代码,但是那种一行代码求解八皇后的代码,这种代码除了自己秀一下智商,你自己想想这种代码是给别人看的吗?

扩展部分:

八皇后问题本质上是一个在解空间中搜索解的过程,除了一一遍历以为,还有一些高效的搜索方式:

1、概率算法

     基本思想:首先应用随机函数在一个8*8的矩阵中放入合适的4个皇后(即一半的皇后)然后再应用之前的回溯的方法进行搜索,随后循环这样的一个过程,当时间容许的情况下,很快就可以找到满足所有的解。当然这种方法对回溯法只是进行了一点点的改动,但时间复杂度上将有很大的提高。

2、A*算法

      这种算法原本是人工智能里求解搜索问题的解法,但八皇后问题也同样是一个搜索问题,因此可以应用这种方法来进行求解。这里关于A*算法的定义以及一些概念就不提供了,大家可以上网进行搜索,网上有很多关于A*算法的详细介绍。如果有兴趣的朋友可以借阅一本人工智能的书籍上面有关于A*算法求解八皇后问题的详细解答过程,同理例如遗传算法,蚁群算法,粒子群算法等都可以求解这类搜索问题

3、广度优先搜索

     这个是和回溯法搜索相反的一种方法,大家如果学过数据结构的应该知道,图的搜索算法有两种,一种是深度优先搜索,二种是广度优先搜索。而前面的回溯算法回归到图搜索的本质后,发现其实是深度优先搜索,因此必定会有广度优先搜索解决八皇后问题,由于应用广度优先搜索解决八皇后问题比应用深度优先搜索其空间复杂度要高很多,所以很少有人去使用这种方法,但是这里我们是来学习算法的,这种算法在此不适合,不一定在其他的问题中就不适合了,有兴趣的朋友可以参考任何一本数据结构的数据上面都有关于广度优先搜索的应用。

 

 

  • 1
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值