力扣 51. N 皇后 II(回溯算法)

417 阅读3分钟

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战

202010171118 52. N皇后 II 01.png

题目描述

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

202010171118 52. N皇后 II 00.png

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

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

示例:

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

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

提示

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

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/n-… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


基于集合判重的回溯算法

国际象棋中,王的女人很厉害,横向、纵向、斜向都能攻击到,现在要为 N 个后宫佳丽搭台唱戏,为了保证后宫安定,需要满足以下四个条件:

  • 每一行只能有一位皇后
  • 每一列只能有一位皇后
  • 左上到右下的主对角线方向只能有一位皇后
  • 右上到左下的副对角线方向只能有一位皇后

由于我们可以在回溯时控制每行放置一位皇后,放完进入下一行皇后的放置流程,所以一行只会出现一个皇后,第一个条件天然满足,只需要保证后三个条件满足即可。

可以使用名叫 colsd1d2 的三个集合来分别记录之前已经用过的列、主对角线、副对角线,新放置皇后的列数、主对角线和副对角线不能在 colsd1d2 中出现。

cols 保存之前已经用过的列数很好理解,直接存 0n - 1 即可。但是主对角线和副对角线是怎么存的?

通过观察我们发现:

  • 位于同一条主对角线上的所有元素,它们的行数和列数之差相等,存 row - col 即可
  • 位于同一条副对角线上的所有元素,它们的行数和列数之和相等,存 row + col 即可

到这里,思路基本就出来了,放出最终解法之前先对照一下我的回溯算法模板

def backtrack(path, state, opts):
    if base:  # 基线条件
        res.append(path)
        return
    for opt in opts:
        if prune:  # 剪枝条件
            continue/break
        # 保存现场(做出选择)
        path.append(opt)
        state = change(state)
        opts.remove(opt)
        # 递归
        backtrack(path, state, opts)
        # 恢复现场(撤销选择)
        path.remove(opt)
        state = unchange(state)
        opts.append(opt)

对于本题:

  • path 路径是不需要的,因为不用记录棋盘摆放,最终只是要统计数字而已
  • state 状态是需要的,用 row 来记录当前要放置是第几行
  • opts 选择集也是需要的,它是由 colsd1d2 三个集合决定的

下面是本题参考解法:

class Solution:
    def totalNQueens(self, n: int) -> int:
        res = 0
        cols, d1, d2 = set(), set(), set()
        def backtrack(row):
            # 基线条件
            if row == n:
                nonlocal res
                res += 1
            for col in range(n):
                # 剪枝条件
                if (col in cols
                or row - col in d1
                or row + col in d2):
                    continue
                # 保存现场
                cols.add(col)
                d1.add(row - col)
                d2.add(row + col)
                # 递归
                backtrack(row + 1)
                # 恢复现场
                cols.remove(col)
                d1.remove(row - col)
                d2.remove(row + col)
        return backtrack(0) or res

运行结果

执行结果:通过
执行用时:56 ms, 在所有 Python3 提交中击败了79.96% 的用户
内存消耗:13.5 MB, 在所有 Python3 提交中击败了9.69% 的用户

本题的时间复杂度是 O(n!)O(n!),一共有 n 层递归,在不考虑斜向的情况下,每层递归可以选择的列数量分别是 n, n - 1, n - 2, ..., 1,所以最多有 n!n! 种组合,于是时间复杂度为 O(n!)O(n!)

另外,本题还可以基于位运算来做,可以参考官方题解的方法二