一.引言
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数组 | |
4 | 4.4107437e-05 | 4.72068786e-05 |
8 | 0.0087618827 | 0.0053482055 |
12 | 6.9231431484 | 2.738075256347 |
随着 n 的增加,移位运算会优于数组遍历。