leetcode题解之52. N皇后 II

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

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

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

示例:

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

 ["..Q.",  // 解法 2
  "Q...",
  "...Q",
  ".Q.."]
]

 

提示:

  • 皇后,是国际象棋中的棋子,意味着国王的妻子。皇后只做一件事,那就是“吃子”。当她遇见可以吃的棋子时,就迅速冲上去吃掉棋子。当然,她横、竖、斜都可走一或七步,可进可退。(引用自 百度百科 - 皇后

直观想法

这个问题是一个经典的问题,感受解法的优雅性很重要。

第一个想法是使用蛮力法,意味着生成在棋盘上放置 N 个皇后的所有可能的情况,并且检查是否保证没有皇后可以互相攻击。这意味着 O(NN)\mathcal{O}(N^N) 的时间复杂度,因此我们必须考虑优化。

下面是两个有用的编程概念。

第一个叫做 约束编程.

它的基本含义是在放置每个皇后以后增加限制。当在棋盘上放置了一个皇后后,立即排除当前行,列和对应的两个对角线。该过程传递了 约束 从而有助于减少需要考虑情况数。

51_pic.png

第二个叫做 回溯法.

我们来想象一下,当在棋盘上放置了几个皇后且不会相互攻击。但是选择的方案不是最优的,因为无法放置下一个皇后。此时我们该怎么做?回溯。意思是回退一步,来改变最后放置皇后的位置并且接着往下放置。如果还是不行,再 回溯

51_backtracking_.png



方法1:回溯

在建立算法之前,我们来考虑两个有用的细节。

一行只可能有一个皇后且一列也只可能有一个皇后。

这意味着没有必要再棋盘上考虑所有的方格。只需要按列循环即可。

对于所有的主对角线有 行号 + 列号 = 常数,对于所有的次对角线有 行号 - 列号 = 常数.

这可以让我们标记已经在攻击范围下的对角线并且检查一个方格 (行号, 列号) 是否处在攻击位置。

51_diagonals.png

现在已经可以写回溯函数 backtrack(row = 0).

  • 从第一个 row = 0 开始.

  • 循环列并且试图在每个 column 中放置皇后.

    • 如果方格 (row, column) 不在攻击范围内

      • (row, column) 方格上放置皇后。
      • 排除对应行,列和两个对角线的位置。
      • If 所有的行被考虑过,row == N
        • 意味着我们找到了一个解
      • Else
        • 继续考虑接下来的皇后放置 backtrack(row + 1).
      • 回溯:将在 (row, column) 方格的皇后移除.

下面是上述算法的一个直接的实现。

1 / 34

class Solution {
  public boolean is_not_under_attack(int row, int col, int n,
                                     int [] rows,
                                     int [] hills,
                                     int [] dales) {
    int res = rows[col] + hills[row - col + 2 * n] + dales[row + col];
    return (res == 0) ? true : false;
  }

public int backtrack(int row, int count, int n,
int [] rows,
int [] hills,
int [] dales)
{
for (int col = 0; col < n; col++) {
if (is_not_under_attack(row, col, n, rows, hills, dales)) {
// place_queen
rows[col] = 1;
hills[row - col + 2 * n] = 1; // "hill" diagonals
dales[row + col] = 1; //"dale" diagonals

    <span class="hljs-comment">// if n queens are already placed</span>
    <span class="hljs-keyword">if</span> (row + <span class="hljs-number">1</span> == n) count++;
    <span class="hljs-comment">// if not proceed to place the rest</span>
    <span class="hljs-keyword">else</span> count = backtrack(row + <span class="hljs-number">1</span>, count, n,
            rows, hills, dales);

    <span class="hljs-comment">// remove queen</span>
    rows[col] = <span class="hljs-number">0</span>;
    hills[row - col + <span class="hljs-number">2</span> * n] = <span class="hljs-number">0</span>;
    dales[row + col] = <span class="hljs-number">0</span>;
  }
}
<span class="hljs-keyword">return</span> count;

}

public int totalNQueens(int n) {
int rows[] = new int[n];
// "hill" diagonals
int hills[] = new int[4 * n - 1];
// "dale" diagonals
int dales[] = new int[2 * n - 1];

<span class="hljs-keyword">return</span> backtrack(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, n, rows, hills, dales);

}
}


class Solution:
def totalNQueens(self, n):
"""
:type n: int
:rtype: int
"""

def is_not_under_attack(row, col):
return not (rows[col] or hills[row - col] or dales[row + col])

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">place_queen</span><span class="hljs-params">(row, col)</span>:</span>
        rows[col] = <span class="hljs-number">1</span>
        hills[row - col] = <span class="hljs-number">1</span>  <span class="hljs-comment"># "hill" diagonals</span>
        dales[row + col] = <span class="hljs-number">1</span>  <span class="hljs-comment"># "dale" diagonals</span>
    
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">remove_queen</span><span class="hljs-params">(row, col)</span>:</span>
        rows[col] = <span class="hljs-number">0</span>
        hills[row - col] = <span class="hljs-number">0</span>  <span class="hljs-comment"># "hill" diagonals</span>
        dales[row + col] = <span class="hljs-number">0</span>  <span class="hljs-comment"># "dale" diagonals</span>
    
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">backtrack</span><span class="hljs-params">(row = <span class="hljs-number">0</span>, count = <span class="hljs-number">0</span>)</span>:</span>
        <span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> range(n):
            <span class="hljs-keyword">if</span> is_not_under_attack(row, col):
                place_queen(row, col)
                <span class="hljs-keyword">if</span> row + <span class="hljs-number">1</span> == n:
                    count += <span class="hljs-number">1</span>
                <span class="hljs-keyword">else</span>:
                    count = backtrack(row + <span class="hljs-number">1</span>, count)
                remove_queen(row, col)
        <span class="hljs-keyword">return</span> count
    
    rows = [<span class="hljs-number">0</span>] * n
    hills = [<span class="hljs-number">0</span>] * (<span class="hljs-number">2</span> * n - <span class="hljs-number">1</span>)  <span class="hljs-comment"># "hill" diagonals</span>
    dales = [<span class="hljs-number">0</span>] * (<span class="hljs-number">2</span> * n - <span class="hljs-number">1</span>)  <span class="hljs-comment"># "dale" diagonals</span>
    <span class="hljs-keyword">return</span> backtrack()

复杂度分析

  • 时间复杂度:O(N!)\mathcal{O}(N!). 放置第 1 个皇后有 N 种可能的方法,放置两个皇后的方法不超过 N (N - 2) ,放置 3 个皇后的方法不超过 N(N - 2)(N - 4) ,以此类推。总体上,时间复杂度为 O(N!)\mathcal{O}(N!) .
  • 空间复杂度:O(N)\mathcal{O}(N) . 需要保存对角线和行的信息。




方法 2:使用 bitmap 回溯

如果你是在面试中 - 使用方法 1

下面的算法有着相同的时间复杂度 O(N!)\mathcal{O}(N!)。但是由于使用了位运算,可以运行得更快。

感谢这个算法的提出者 takaken.

为了便于理解该算法,下面的代码进行了逐步解释。

class Solution {
  public int backtrack(int row, int hills, int next_row, int dales, int count, int n) {
    /** 
     row: 当前放置皇后的行号
     hills: 主对角线占据情况 [1 = 被占据,0 = 未被占据]
     next_row: 下一行被占据的情况 [1 = 被占据,0 = 未被占据]
     dales: 次对角线占据情况 [1 = 被占据,0 = 未被占据]
     count: 所有可行解的个数
     */
<span class="hljs-comment">// 棋盘所有的列都可放置,</span>
<span class="hljs-comment">// 即,按位表示为 n 个 '1'</span>
<span class="hljs-comment">// bin(cols) = 0b1111 (n = 4), bin(cols) = 0b111 (n = 3)</span>
<span class="hljs-comment">// [1 = 可放置]</span>
<span class="hljs-keyword">int</span> columns = (<span class="hljs-number">1</span> &lt;&lt; n) - <span class="hljs-number">1</span>;

<span class="hljs-keyword">if</span> (row == n)   <span class="hljs-comment">// 如果已经放置了 n 个皇后</span>
  count++;  <span class="hljs-comment">// 累加可行解</span>
<span class="hljs-keyword">else</span> {
  <span class="hljs-comment">// 当前行可用的列</span>
  <span class="hljs-comment">// ! 表示 0 和 1 的含义对于变量 hills, next_row and dales的含义是相反的</span>
  <span class="hljs-comment">// [1 = 未被占据,0 = 被占据]</span>
  <span class="hljs-keyword">int</span> free_columns = columns &amp; ~(hills | next_row | dales);

  <span class="hljs-comment">// 找到可以放置下一个皇后的列</span>
  <span class="hljs-keyword">while</span> (free_columns != <span class="hljs-number">0</span>) {
    <span class="hljs-comment">// free_columns 的第一个为 '1' 的位</span>
    <span class="hljs-comment">// 在该列我们放置当前皇后</span>
    <span class="hljs-keyword">int</span> curr_column = - free_columns &amp; free_columns;

    <span class="hljs-comment">// 放置皇后</span>
    <span class="hljs-comment">// 并且排除对应的列</span>
    free_columns ^= curr_column;

    count = backtrack(row + <span class="hljs-number">1</span>,
            (hills | curr_column) &lt;&lt; <span class="hljs-number">1</span>,
            next_row | curr_column,
            (dales | curr_column) &gt;&gt; <span class="hljs-number">1</span>,
            count, n);
  }
}

<span class="hljs-keyword">return</span> count;

}
public int totalNQueens(int n) {
return backtrack(0, 0, 0, 0, 0, n);
}
}


class Solution:
def totalNQueens(self, n):
"""
:type n: int
:rtype: int
"""

def backtrack(row = 0, hills = 0, next_row = 0, dales = 0, count = 0):
"""
:type row: 当前放置皇后的行号
:type hills: 主对角线占据情况 [1 = 被占据,0 = 未被占据]
:type next_row: 下一行被占据的情况 [1 = 被占据,0 = 未被占据]
:type dales: 次对角线占据情况 [1 = 被占据,0 = 未被占据]
:rtype: 所有可行解的个数
"""

if row == n: # 如果已经放置了 n 个皇后
count += 1 # 累加可行解
else:
# 当前行可用的列
# ! 表示 0 和 1 的含义对于变量 hills, next_row and dales的含义是相反的
# [1 = 未被占据,0 = 被占据]
free_columns = columns & ~(hills | next_row | dales)

            <span class="hljs-comment"># 找到可以放置下一个皇后的列</span>
            <span class="hljs-keyword">while</span> free_columns:
                <span class="hljs-comment"># free_columns 的第一个为 '1' 的位</span>
                <span class="hljs-comment"># 在该列我们放置当前皇后</span>
                curr_column = - free_columns &amp; free_columns
                
                <span class="hljs-comment"># 放置皇后</span>
                <span class="hljs-comment"># 并且排除对应的列</span>
                free_columns ^= curr_column
                
                count = backtrack(row + <span class="hljs-number">1</span>, 
                                  (hills | curr_column) &lt;&lt; <span class="hljs-number">1</span>, 
                                  next_row | curr_column, 
                                  (dales | curr_column) &gt;&gt; <span class="hljs-number">1</span>, 
                                  count)
        <span class="hljs-keyword">return</span> count

    <span class="hljs-comment"># 棋盘所有的列都可放置,</span>
    <span class="hljs-comment"># 即,按位表示为 n 个 '1'</span>
    <span class="hljs-comment"># bin(cols) = 0b1111 (n = 4), bin(cols) = 0b111 (n = 3)</span>
    <span class="hljs-comment"># [1 = 可放置]</span>
    columns = (<span class="hljs-number">1</span> &lt;&lt; n) - <span class="hljs-number">1</span>
    <span class="hljs-keyword">return</span> backtrack()

复杂度分析

  • 时间复杂度:O(N!)\mathcal{O}(N!).
  • 空间复杂度:O(N)\mathcal{O}(N).
posted @ 2021-02-27 16:08  乐天的java  阅读(136)  评论(0)    收藏  举报