面试准备——动态规划(2):八皇后问题/n皇后问题

题目:在8×8的国际象棋上摆放八个皇后,使其不能相互攻击,即任意两个皇后不得处在同一行、同一列或者同一对角斜线上。下图中的每个黑色格子表示一个皇后,这就是一种符合条件的摆放方法。请求出总共有多少种摆法。(leetcode51、52)
leetcode代码网址:https://github.com/vivianLL/LeetCode

解法一:回溯法

由于每次都是遍历下一行,所以两个皇后的行肯定不同;因此判断当前列是否已经占用,和判断对角线的位置。
用三个数组来表示列、正反对角线的占用情况。一行行的遍历,如果没有冲突就把相应的位置置为占用,继续处理下一行,并记录改行的皇后放在了哪一列,当皇后都放完后,根据记录的列号来拼出结果。进行回溯时要把占用的位置还回去。对角线位置的计算要小心(尤其是反对角线),可以把顶点带进去计算验证一下。
python实现如下:

def totalNQueens(self, n: int) -> int:
    self.col = [False] * n
    self.diag = [False] * (2 * n)
    self.anti_diag = [False] * (2 * n)
    self.result = 0
    # self.result = []
    self.recursive(0, n, [])
    return self.result

def recursive(self, row, n, column):
    if row == n:
        self.result += 1
        # self.result.append(list(map(lambda x: '.' * x + 'Q' + '.' * (n - 1 - x), column)))
    else:
        for i in range(n):
            if not self.col[i] and not self.diag[row + i] and not self.anti_diag[n - i + row]:
                self.col[i] = self.diag[row + i] = self.anti_diag[n - i + row] = True
                self.recursive(row + 1, n, column + [i])
                self.col[i] = self.diag[row + i] = self.anti_diag[n - i + row] = False

解法二:位运算

首先,我们的基本思路与传统一样,每一行只能放并且必须放一个皇后。每次放置一个皇后之后,它就会对后面所有行中,可能放置皇后的位置产生影响。如下图所示,我们已经放置了三个皇后Q1,Q2,Q3。 接着在下面一行放置Q4时,用彩色标注出的区域都是不可行的方案,会与已经放置的三个皇后产生冲突。
在这里插入图片描述
这个冲突其实有三种不同的情况,我们用三个变量A,B,C分别来表示这三种不同的冲突。这三个变量都是一个八位的二进制数,这八位中,为1的表示有冲突,为0表示没有冲突。

  1. 与已放置的皇后处于同一列中;
    我们用B表示这种冲突。例如在图中,第四行会有三个位置因为列攻击而不能再放置皇后(图中大红色方块),因此A = 1000 1001。
  2. 与已放置的皇后处于同一左斜(指向右下方)对角线中;
    我们用A表示这种冲突。例如在图中,蓝色方块所表示的位置,分别是由于Q1和Q2的左斜对角线上的攻击而不能够放置新的皇后, 因此B = 0001 0010。
  3. 与已放置的皇后处于同一右斜(指向右上方)对角线中。
    我们用C表示这种冲突。例如图中粉色方块, 是由Q2的右斜对角线的攻击造成的。因此C = 0010 0000。

有了A,B,C,三个变量,我们很容易求出当前这一行中,有哪些位置是可以放置皇后的。这个变量我们用D来表示,我们可以写出D = ~(A|B|C)。D中为1的那些位置则是我们可以放置皇后的位置。在上图的情况下,D = 0100 0100。
此时,我们有两个可能的放置皇后的位置,那么要如何取出第一个位置呢?这里有一个技巧。我们用bit表示可能的一个位置,使用bit = D & (-D)即可取出D中最右边的一个1。例如D = 0100 0100,则-D = 10111100(注意计算机中的数都是补码)。而bit = D & (-D) = 0000 0100 取出了最右边的那个1。
注意这个结果中,除了最右 n 位以外,左边的位中也会有一些 1,这些 1 是多余的,应当去掉。怎么去掉呢?可以用一个最右 n 位为 1、其它位为 0 的 bit array 与上述结果进行与运算,而这个 bit array 可以用 (1 << n) - 1 这个表达式制造出来。
最后还有一点,如何递归?递归中传入的参数(也即A,B,C)该如何设定?其实也很简单,特别是对于列冲突变量B来说,只要使用B|bit即可表示下一行列冲突的情况,对于A,采用(A|bit)<<1,对于C,采用(C|bit>>1)即可表示下一行中,对角线冲突的情况。
(0按位取反为-1,-1按位取反为0,-1和任何数按位与的结果是任何数,1与任何数按位与的结果是1)
python代码如下:

def totalNQueens(self, n: int) -> int:
    self.count = 0

    def DFS(n, row, cols, pie, na):
        bits = (~(cols | pie | na)) & ((1 << n) - 1)  # 可以放置皇后的位置,对按列冲突、按左右斜对角线冲突的位置取反
        while bits:
            p = bits & -bits       # 取出可以放置皇后的位置中最右边的一个1
            if row == n - 1:
                self.count += 1
            else:
                DFS(n, row + 1, cols | p, (pie | p) << 1, (na | p) >> 1)  # 下一行中列、左斜对角线、右斜对角线冲突的情况
            bits = bits & (bits - 1)
    DFS(n, 0, 0, 0, 0)
    return self.count

参考网址:
LeetCode N-Queens
八皇后问题优雅解法——位运算
10394 用位运算速解 n 皇后问题 (非常好)

发布了143 篇原创文章 · 获赞 161 · 访问量 29万+

猜你喜欢

转载自blog.csdn.net/vivian_ll/article/details/93169434