春招面试之N皇后问题

  • 2019 年 10 月 6 日
  • 笔记

手撕算法系列之N皇后问题

0.题目

关于N皇后总共有两道题:

第一道题:求出所有皇后

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

上图为 8 皇后问题的一种解法。

给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。

每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。

示例:

输入: 4  输出: [   [".Q..",  // 解法 1    "...Q",    "Q...",    "..Q."],     ["..Q.",  // 解法 2    "Q...",    "...Q",    ".Q.."]  ]  解释: 4 皇后问题存在两个不同的解法。  

第二道题:求出所有满足皇后的解法个数

1.深度优先搜索

思想

使用一个数组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)+"种解决方案")  

2.空间换时间

思想

注意到皇后位置的限制条件的本质,其实就是说每一竖、每一撇、每一捺上只能有一个皇后。

当试探一个位置时,如果能够立即知道它所在的竖、撇、捺是否已被占用,就可以在 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))  

3.位运算必杀技

为了更好的减少空间内存,最后采用位运算来实现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))