关于N皇后总共有两道题:
第一道题:求出所有皇后
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
上图为 8 皇后问题的一种解法。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
示例:
输入: 4
输出: [
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解释: 4 皇后问题存在两个不同的解法。
第二道题:求出所有满足皇后的解法个数
【思想】
使用一个数组queencol
表示某一行已经被皇后占据的列,从上往下依次试探每行皇后可以放在哪些行。每次试探都与前面所有放好的皇后检查是否有冲突。
假设当前试探的位置为 (col,row),而第 i 行已经有一个皇后放置在 (queencol[i],i)。这两个皇后有三种冲突的可能:
col == queencol[i]
col-queencol[i] == i-row
col-queencol[i] == row-i
同一列好说,那撇捺怎么是上述式子呢?
建立如图所示的坐标系,设第2行(index从0开始)皇后坐标为(col,row)。那么撇就是蓝色线。捺就是橙色线。
上面已经假设,第 i 行已经有一个皇后放置在 (queencol[i],i),那么由数学基础知识,线性斜率知道斜率如何求,而如上图可知道,斜率分别为1(捺)与-1(撇)。
上述问题就转化为:两点坐标求斜率,点A(col,row),点B(queencol[i],i)。
那么对于撇:
(i-row)/(queencol[i]-col)=-1 ,也就是i-row = col-queencol[i],两边变形得到:col-queencol[i] == i-row
对于捺:
(i-row)/(queencol[i]-col)=1,也就是i-row = queencol[i]-col,两边变形得到:col-queencol[i] == row-i
下面就来实现一下。
【实现】
def DFS(n,row,cur_res):
global count
# 递归终止条件
if row>n-1:
print(cur_res)
count+=1
res.append(cur_res)
print(res)
for col in range(n):
# 检测当前列冲突
ok = True
for i in range(row):
# 列冲突与撇、捺冲突queencol
if col == queencol[i] or col-queencol[i] == row-i or col-queencol[i] == i-row:
ok = False
break
# 判断当前列是否冲突
if not ok:
continue
# 当前列无冲突,皇后占据当前位置
queencol[row]=col
# 递归下一行
DFS(n,row+1,cur_res+[col])
n = 4 # 棋盘大小
queencol = [0]*n # 某一行已经被皇后占据的列
count = 0
res = []
DFS(n,0,[])
print("总共有"+str(count)+"种解决方案")
【思想】
注意到皇后位置的限制条件的本质,其实就是说每一竖、每一撇、每一捺上只能有一个皇后。
当试探一个位置时,如果能够立即知道它所在的竖、撇、捺是否已被占用,就可以在 O(1) 的时间内检查冲突了。
为此,将刚刚放置的皇后所在的竖、撇、捺标记为已占用,并在调用返回之后清除标记。
对于撇捺上述我们知道它们的规律,上述的规律,同时还可以得到撇捺的另一个规律:
撇:行+列=一个常数
捺:行-列=一个常数
在对冲突存储的时候,可以采用布尔来判断,也可以用set集合判断,下面给出两种解决方案!
【集合空间换时间】
def DFS(n,row,queencol):
global count
# 递归终止条件
if row>n-1:
print(queencol)
res.append(queencol)
count+=1
for col in range(n):
# 碰撞判断
if col in shu or (row+col) in pie or (row-col) in na:
continue
shu.add(col)
pie.add(row+col)
na.add(row-col)
DFS(n,row+1,queencol+[col])
# 递归回去清理空间
shu.remove(col)
pie.remove(row+col)
na.remove(row-col)
n = 4
shu = set()
pie = set()
na = set()
count = 0
res = []
DFS(n,0,[])
print("总共有"+str(count)+"种解决方案")
print(res)
print(len(res))
【布尔空间换时间】
这个方法申请撇捺空间大小的时候,可以推一下,都为2*n-1
。
def DFS(n,row,queencol):
global count
# 递归终止条件
if row>n-1:
print(queencol)
res.append(queencol)
count+=1
for col in range(n):
j = row+col
k = col-row
if shu[col] or pie[j] or na[k]:
continue
shu[col]=pie[j]=na[k]=True
DFS(n,row+1,queencol+[col])
shu[col]=pie[j]=na[k]=False
n = 4
shu = [False]*n
pie = [False]*(2*n-1)
na = [False]*(2*n-1)
count = 0
res = []
DFS(n,0,[])
print("总共有"+str(count)+"种解决方案")
print(res)
print(len(res))
为了更好的减少空间内存,最后采用位运算来实现N皇后!
如果我们已经知道每一行的空位,也就是皇后可取位置,那么时间复杂度则会大大降低,基于这个思想,通过位运算获取每一行的空位,来优化算法。
位运算常用:
X&1==1 or ==0
x = x & (x-1) => 清零最低位的1 例如: x=110 x-1=101 x&x-1=100
x & -x => 得到最低位的1
x & ~x => 0
【代码实现】
位运算常用:
X&1==1 or ==0
x = x & (x-1) => 清零最低位的1 例如: x=110 x-1=101 x&x-1=100
x & -x => 得到最低位的1
x & ~x => 0
注意一点,在需要通过math函数来求当前p为2的多少次方,而这个多少次方就是我们每次皇后存放的真实列。
import math
def DFS(n,row,col,pie,na,queencol):
global count
# 递归终止条件
if row>n-1:
print(queencol)
res.append(queencol)
count+=1
bits = (~(col|pie|na)) & ((1<<n)-1) # 得到当前行的空位
while bits:
p = bits&-bits # 取最低位1
DFS(n,row+1,col|p,(pie|p)<<1,(na|p)>>1,queencol+[int(math.log(p,2))])
bits = bits&(bits-1)
n = 4
count = 0
res = []
DFS(n,0,0,0,0,[])
print("总共有"+str(count)+"种解决方案")
print(res)
print(len(res))