Python - N 皇后解法 数组 & 移位

一.引言

N 皇后问题又叫 8皇后问题,研究的是如何将 n 个皇后放置于 n x n 的棋盘上,并且找到使 n 个皇后无法互相攻击的不同摆法。按照国际象棋的玩法,如果两个皇后无法互相攻击,则需要满足以下条件 :

A.不在同一行

B.不在同一列

C.不在同一对角线

D.不在同一反对角线

二.数组标记法

1.代码思路

N 皇后要求同一行列不能有其他皇后,对角线上不能有其他皇后,所以可以初始化一个 N x 1 的数组,数组的 index 代表棋盘的 row,数组 index 对应的元素代表棋盘的 col,row-col 代表皇后放置的位置

(1) 初始化 Nx1 的数组保存每行存储的皇后与坐标 row-col

(2) 遍历 row,col,判断皇后是否可以加入当前位置,需要不同行,不同列,不同正反对角线

(3) 满足条件,row += 1 寻找下一行可以放置皇后的位置,不满足条件,回朔继续判断

(4) 当 row >= N 时,代表当前数组存储的皇后达到 N 个,满足条件

2.代码实现

-> 检查当前位置是否可以摆放 Queen

# 检查当前位置是否可以放置 Queen
def check(board, row, col):
    for i in range(row):
        # 不在同一列 不在同一对角线
        if abs(board[i] - col) == 0 or abs(board[i] - col) == abs(i - row):
            return False
    return True

check 函数用于检查当前 row ,col 是否可以摆放皇后,再看下引言中放置皇后需要满足的 4 个条件:

A.不在同一行 : 递归时 row + 1,所以遍历时自动实现了每一行只有一个皇后

B.不在同一列:board[i] - col == 0 ,如果当前位置与之前放置的皇后列相同就会返回 False

C.不在同一对角线:对于中间的位置,其存在左 45 度和右 45 度两个对角线,这里采用斜率的计算方法, board[i] - col == i - row 代表相同方向的 45 度对角线

D.不在同一反对角线:board[i] - col = -1 * (i - row) 代表不同方向的 45 度对角线,因为斜率的绝对值相同,所以 C,D 两个条件合在了一起 ,变为 abs(board[i] - col) == abs(i - row)

-> 主函数

前面提到,row >= border 时,代表皇后摆放完毕,所以可以输出一次结果,或者代表找到一种解法,随后代码回朔,继续判断其他可能直到结束,如果 check(board,row,col) 通过,代表当前row-col 可以摆放皇后,所以 board[row] = col,同时 row + 1,代表去寻找下一行 row 满足摆放要求的位置,每递归一层,check 的 board 就会多一个皇后,因为是从 for i in range(row),这个 row 就是当前最新放置皇后的位置。

def eightQueen(board, row):
    # 皇后个数
    border = len(board)
    # 当 row >= border 时,代表此时每行都能摆放皇后,即获得解法
    if row >= border:
        for i, col in enumerate(board):
            print('□ ' * col + '♝ ' + '□ ' * (len(board) - 1 - col))
        print("")
    # 逐列遍历
    for col in range(border):
        if check(board, row, col):
            # board 的key代表皇后保存的row,value代表皇后保存的col
            board[row] = col
            eightQueen(board, row + 1)


_board = [0 for i in range(4)]
eightQueen(_board, 0)
□ ♝ □ □ 
□ □ □ ♝ 
♝ □ □ □ 
□ □ ♝ □ 

□ □ ♝ □ 
♝ □ □ □ 
□ □ □ ♝ 
□ ♝ □ □ 

三.二进制标记法

1.代码实现

前面写布隆过滤器的时候讲到过 BitMap 的构建与实现 ,这里有一个思想就是,用数组索引存储的结构,都可以转换为 bit 数组存储,其好处是可以节约空间,如果说坏处的话,就是代码的可读性比较差,因为代码涉及到了二进制的操作,一些二进制的操作以及bit数组相关的知识可以参考上面提到的文章,可以更好的理解下述方法:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
class Solution(object):
    def __init__(self):
        self.count = 0

    # 深度搜索
    # 参数: row n column diag antiDiag
    def totalNQueens(self, n):
        self.dfs(0, n, 0, 0, 0)
        return self.count

    def dfs(self, row, n, column, diag, antiDiag):
        if row == n:
            self.count += 1
            return
        # 每一最多一个 因为任意一个皇后不能在同行 同列 同对角线
        for index in range(n):
            # 列上是否安全
            tmp_col = (1 << index)
            isColSafe = tmp_col & column == 0
            # 对角线是否安全
            tmp_diag = (1 << (n - 1 + row - index))
            isDigSafe = tmp_diag & diag == 0
            # 反对角线是否安全
            tmp_anti_diag = (1 << (row + index))
            isAntiDiagSafe = tmp_anti_diag & antiDiag == 0
            if isAntiDiagSafe and isColSafe and isDigSafe:
                self.dfs(row + 1, n, tmp_col | column,
                         tmp_diag | diag,
                         tmp_anti_diag | antiDiag)


if __name__ == '__main__':
    s = Solution()
    print(s.totalNQueens(4))

2.代码思路

-> 整体分析

这里主体代码是 dfs,上面数组的方法记录了每个可以存放皇后的 row-col,再遍历下一个位置时,与当前已知皇后的 row-col 进行 check,判断 A、B、C、D 四个条件是否满足,随后继续递归,使用二进制 bit 数组可以简化 check 的操作,对于一个 N x N 的表格,其有 n 行 - row,n列 - col,2n-2 个正对角线 - diag,2n-2 个反对角线 - anti_diag,每存储一个皇后,对应的 bit 数组的 0 变为 1 标记此处已无法放置皇后,下面是正向 diag 和反向 anti-diag 的示意图 :

正向 diag 下,每一个对角线的编号与 row-col 的关系:

diag_index = row + col

反向 diag 下,每一个对角线的编号与 row-col 的关系:

anti_diag_index = n - 1 - row + col

-> check 函数

结合上面两幅图分析一下代码的执行流程,还是老规矩,首先放置皇后需要满足 A,B,C,D 四个条件

A.不在同一行:上述代码 for index in range(n) 自动实现了行 row 的去重

B.不在同一列:共有n列,所一构造 n 个 0 组成的 bit 数组,其值最大为 1 << (n-1)

C.不在同一正对角线:对角线共有 2n - 1 个,编号为 0 - (2n-2),所以其值最大为 1 << (2n-2),对应 2n-1 个位置有 0,1 两种可能,1 代表当前对角线有皇后,0 代表当前对角线没有皇后,判断当前位置与当前正对角线集合有没有交集 (皇后位置是否重叠),只需要执行 & 操作,假设 0010 & 0000 = 0000 = 0 ,代表二者位置没有重叠可以放置,而如果是 0010 & 0010 = 0010 = 2 != 0 则代表有冲突,因为第二个位置都为1,说明该位置皇后重叠,所以对角线的 check 由之前的求斜率绝对值变为了位运算,位运算不了解的同学可以回看下上面提到的 bitMap 构建,非常好理解。

D.不在同一反对角线:与不在同一对角线判断方法相同,也是构造通过二进制移位操作实现 check,其值最大为 1 << (2n-2),判断方法这里不多赘述

所以下述代码其实和上面数组判断 n 皇后的check函数是相同思路,这里 << 移位操作后面对应的数字和上面图中给出的索引相匹配:

至于上面提到的最大值,可以加下述打印查看:

n=4 时,col 的最大值 1 << (4-1) = 8 ,diag = antiDiag 的最大值 1 << (2 * 4 - 2) = 64

print("row:", row, " col:", column, " diag:", tmp_diag, "antiDiag:", tmp_anti_diag, " isCol: ", isColSafe, str(1 << index), " isDiag:", isDigSafe, str(1 << (n - 1 + row - index))," isAntiDiag:", isAntiDiagSafe, str(1 << (row + index)))

 

-> 递归调用

递归调用 row + 1 这个好理解,因为是一行一行理解,| 操作对应的是向 Bit 数组对应索引添加元素,上面 check 成功后,对应位置的二进制元素需要从 0 变成 1,这里就需要用到 | 操作,假设当前 diag 为 0010,第二个位置已经没法放皇后了,新来一个皇后,其位置为第三位 0100 ,执行 | 操作后,新的 diag 示意为:0010 | 0100 = 0110 ,代表第二,第三个位置都不可以用了,这样后续递归时,上一步添加位置的信息也得以传递

 -> 满足条件

和上面类似,如果 row 递归到 n,说明前面有 n 个皇后完成了 check,所以当前 n 个皇后可以构成一种解法。这里先讲代码,再讲代码思路是因为如果一上来就是 二进制 操作没有前因后果会十分难以阅读理解。如果想要像上面代码一样展示数据位置,可以做如下修改:

增加 result 存储结果

if row == n:
    self.count += 1
    for i, col in enumerate(self.result):
        print('□ ' * col + '♝ ' + '□ ' * (len(self.result) - 1 - col))
    print("")
    self.result = []
    return

递归添加到数组,如果回朔则 pop 弹出 

if isAntiDiagSafe and isColSafe and isDigSafe:
    self.result.append(int(math.log(tmp_col, 2)))
    self.dfs(row + 1, n, tmp_col | column,
             tmp_diag | diag,
             tmp_anti_diag | antiDiag)
    if len(self.result) != 0:
        self.result.pop()

位置和可能数就搞定了:

□ ♝ □ □ 
□ □ □ ♝ 
♝ □ □ □ 
□ □ ♝ □ 

□ □ ♝ □ 
♝ □ □ □ 
□ □ □ ♝ 
□ ♝ □ □ 

2

四.总结

两种算法通过数组和 bit 数组实现了对状态的保存,下面看下执行效率:

数组bit数组
44.4107437e-05
 
4.72068786e-05
 
80.00876188270.0053482055
126.9231431484
 
2.738075256347
 

随着 n 的增加,移位运算会优于数组遍历。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BIT_666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值