加载中...
51-N 皇后(N-Queens)
发表于:2021-12-03 | 分类: 困难
字数统计: 310 | 阅读时长: 1分钟 | 阅读量:

原文链接: https://leetcode-cn.com/problems/n-queens

英文原文

The n-queens puzzle is the problem of placing n queens on an n x n chessboard such that no two queens attack each other.

Given an integer n, return all distinct solutions to the n-queens puzzle. You may return the answer in any order.

Each solution contains a distinct board configuration of the n-queens' placement, where 'Q' and '.' both indicate a queen and an empty space, respectively.

 

Example 1:

Input: n = 4
Output: [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
Explanation: There exist two distinct solutions to the 4-queens puzzle as shown above

Example 2:

Input: n = 1
Output: [["Q"]]

 

Constraints:

  • 1 <= n <= 9

中文题目

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

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

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

 

示例 1:

输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:

输入:n = 1
输出:[["Q"]]

 

提示:

  • 1 <= n <= 9
  • 皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上。

通过代码

高赞题解

解题思路:

一句话题解:回溯算法是一种遍历算法,以 深度优先遍历 的方式尝试所有的可能性。有些教程上也叫「暴力搜索」。回溯算法是 有方向地 搜索,区别于多层循环实现的暴力法。

许多复杂的、规模较大的问题都可以使用回溯法,有「通用解题方法」的美称。(来自 百度百科

说明:



思路分析:以 $4$ 皇后问题为例,它的「搜索」过程如下。大家可以在纸上模拟下面这个过程:

<幻灯片1.png,幻灯片2.png,幻灯片3.png,幻灯片4.png,幻灯片5.png,幻灯片6.png,幻灯片7.png,幻灯片8.png,幻灯片9.png,幻灯片10.png,幻灯片11.png,幻灯片12.png,幻灯片13.png,幻灯片14.png,幻灯片15.png,幻灯片16.png,幻灯片17.png,幻灯片18.png,幻灯片19.png,幻灯片20.png,幻灯片21.png,幻灯片22.png,幻灯片23.png,幻灯片24.png,幻灯片25.png,幻灯片26.png,幻灯片27.png,幻灯片28.png,幻灯片29.png,幻灯片30.png>

(早期写的题解配色较差,请大家谅解。)

理解树形结构

先尝试画出递归树,以 $4$ 皇后问题为例,画出的递归树如下:

image.png

搜索的过程蕴含了 剪枝 的思想。「剪枝」的依据是:题目中给出的 「N 皇后」 的摆放规则:1、不在同一行;2、不在同一列;3、不在同一主对角线方向上;4、不在同一副对角线方向上。

小技巧:记住已经摆放的皇后的位置

这里记住已经摆放的位置不能像 Flood Fill 一样,简单地使用 visited 布尔数组。放置的规则是:一行一行考虑皇后可以放置在哪一个位置上,某一行在考虑某一列是否可以放置皇后的时候,需要根据前面已经放置的皇后的位置。

由于是一行一行考虑放置皇后,摆放的这些皇后肯定不在同一行,为了避免它们在同一列,需要一个长度为 $N$ 的布尔数组 cols,已经放置的皇后占据的列,就需要在对应的列的位置标注为 True

考虑对角线(找规律)

下面我们研究一下主对角线或者副对角线上的元素有什么特性。在每一个单元格里写下行和列的 下标

image.png

为了保证至少两个皇后不同时出现在 同一主对角线方向 或者 同一副对角线方向。检查策略是,只要「检测」到新摆放的「皇后」与已经摆放好的「皇后」冲突,就尝试摆放同一行的下一个位置,到行尾还不能放置皇后,就退回到上一行

可以像全排列 used 数组那样,再为 「主对角线(Main diagonal)」 和 「副对角线(Sub diagonal)」 设置相应的 布尔数组变量,只要排定一个 「皇后」 的位置,就需要占住对应的位置。

编码

我们使用一个 $1$ 到 $4$ 的排列表示一个 $4 \times 4$ 的棋盘,例如:

image.png{:width=”500px”}

得到一个符合要求的全排列以后,生成棋盘的代码就很简单了。

说明

  • 将「行状态」、「主对角线状态」、「副对角线状态」 设置为成员变量,以避免递归方法参数冗长(参考代码 2、参考代码 3 类似看待)。至于是否有必要这样做,以后在项目开发中需要遵守项目文档的规定;
  • Java 中 Stack 已经废弃,推荐使用 ArrayDeque,可以查阅文档。

方法:回溯搜索算法(深度优先遍历)

下面虽然给出了 3 版代码,但都只是在如何记住已经摆放的皇后的位置上做文章,代码结构都一样,大家只看不同的部分就可以了。

参考代码 1

[]
import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.List; public class Solution { private int n; /** * 记录某一列是否放置了皇后 */ private boolean[] col; /** * 记录主对角线上的单元格是否放置了皇后 */ private boolean[] main; /** * 记录了副对角线上的单元格是否放置了皇后 */ private boolean[] sub; private List<List<String>> res; public List<List<String>> solveNQueens(int n) { res = new ArrayList<>(); if (n == 0) { return res; } // 设置成员变量,减少参数传递,具体作为方法参数还是作为成员变量,请参考团队开发规范 this.n = n; this.col = new boolean[n]; this.main = new boolean[2 * n - 1]; this.sub = new boolean[2 * n - 1]; Deque<Integer> path = new ArrayDeque<>(); dfs(0, path); return res; } private void dfs(int row, Deque<Integer> path) { if (row == n) { // 深度优先遍历到下标为 n,表示 [0.. n - 1] 已经填完,得到了一个结果 List<String> board = convert2board(path); res.add(board); return; } // 针对下标为 row 的每一列,尝试是否可以放置 for (int j = 0; j < n; j++) { if (!col[j] && !main[row - j + n - 1] && !sub[row + j]) { path.addLast(j); col[j] = true; main[row - j + n - 1] = true; sub[row + j] = true; dfs(row + 1, path); sub[row + j] = false; main[row - j + n - 1] = false; col[j] = false; path.removeLast(); } } } private List<String> convert2board(Deque<Integer> path) { List<String> board = new ArrayList<>(); for (Integer num : path) { StringBuilder row = new StringBuilder(); row.append(".".repeat(Math.max(0, n))); row.replace(num, num + 1, "Q"); board.add(row.toString()); } return board; } }

参考代码 2:其实已经摆放皇后的列下标、占据了哪一条主对角线、哪一条副对角线也可以使用哈希表来记录。

实际上哈希表底层也是数组,使用哈希表可以不用处理已经占据位置的皇后的主对角线、副对角线的下标偏移问题。

[]
import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.HashSet; import java.util.List; import java.util.Set; public class Solution { private Set<Integer> col; private Set<Integer> main; private Set<Integer> sub; private int n; private List<List<String>> res; public List<List<String>> solveNQueens(int n) { this.n = n; res = new ArrayList<>(); if (n == 0) { return res; } col = new HashSet<>(); main = new HashSet<>(); sub = new HashSet<>(); Deque<Integer> path = new ArrayDeque<>(); dfs(0, path); return res; } private void dfs(int row, Deque<Integer> path) { if (row == n) { List<String> board = convert2board(path); res.add(board); return; } // 针对每一列,尝试是否可以放置 for (int i = 0; i < n; i++) { if (!col.contains(i) && !main.contains(row - i) && !sub.contains(row + i)) { path.addLast(i); col.add(i); main.add(row - i); sub.add(row + i); dfs(row + 1, path); sub.remove(row + i); main.remove(row - i); col.remove(i); path.removeLast(); } } } private List<String> convert2board(Deque<Integer> path) { List<String> board = new ArrayList<>(); for (Integer num : path) { StringBuilder row = new StringBuilder(); row.append(".".repeat(Math.max(0, n))); row.replace(num, num + 1, "Q"); board.add(row.toString()); } return board; } }

参考代码 3:搜索问题一般来说复杂度很高,因此在线测评系统的后台测试数据不会很大。因此布尔数组可以只用一个整数来代替(int 或者 long 根据情况决定),int 类型整数等价于一个 $32$ 位布尔数组,long 类型整数等价于一个 $64$ 位布尔数组。

使用一个整数代表一个布尔数组,在比较布尔数组所有的位的值是否相等时,只需要 $O(1)$,并且传递参数、复制也是相对方便的。这样的技巧叫做「状态压缩」,动态规划问题里有一类问题就叫做状态压缩 dp(我学不过来了,以后学会了再向大家介绍,请谅解)。

状态压缩的缺点是:编码得很细心,不容易调试。

「力扣」第 1371 题:每个元音包含偶数次的最长子字符串,是每日一题出现过的状态压缩的经典问题,综合使用了很多算法思想和技巧,感兴趣的朋友可以复习一下。

说明:使用 状态压缩 技巧,可以完成 「力扣」第 52 题:「N皇后 II」

[]
import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.List; public class Solution { private List<List<String>> res; private int n; public List<List<String>> solveNQueens(int n) { this.n = n; res = new ArrayList<>(); if (n == 0) { return res; } int col = 0; int main = 0; int sub = 0; Deque<Integer> path = new ArrayDeque<>(); dfs(0, col, main, sub, path); return res; } private void dfs(int row, int col, int sub, int main, Deque<Integer> path) { if (row == n) { List<String> board = convert2board(path); res.add(board); return; } // 针对每一列,尝试是否可以放置 for (int i = 0; i < n; i++) { if (((col >> i) & 1) == 0 && ((main >> (row - i + n - 1)) & 1) == 0 && ((sub >> (row + i)) & 1) == 0) { path.addLast(i); col ^= (1 << i); main ^= (1 << (row - i + n - 1)); sub ^= (1 << (row + i)); dfs(row + 1, col, sub, main, path); sub ^= (1 << (row + i)); main ^= (1 << (row - i + n - 1)); col ^= (1 << i); path.removeLast(); } } } private List<String> convert2board(Deque<Integer> path) { List<String> board = new ArrayList<>(); for (Integer num : path) { StringBuilder row = new StringBuilder(); row.append(".".repeat(Math.max(0, n))); row.replace(num, num + 1, "Q"); board.add(row.toString()); } return board; } }

同类问题

参考资料

  • liuyubobobo 老师在慕课网上开设的课程《玩转算法面试》代码仓库
  • 《剑指 Offer(第 2 版)》面试题 38 :字符串的排列,相关题目 2。

一个回溯算法可视化的小项目

通过可视化帮助理解回溯算法的思想。写 Java 的朋友可以看看,这是我写的一个练习的项目,学习的价值不大,不用点赞。

...-09-03 上午3.20.54.mov

...-09-03 上午3.22.16.mov

统计信息

通过次数 提交次数 AC比率
165769 224763 73.8%

提交历史

提交时间 提交结果 执行时间 内存消耗 语言

相似题目

题目 难度
N皇后 II 困难
网格照明 困难
上一篇:
50-Pow(x, n)
下一篇:
52-N皇后 II(N-Queens II)
本文目录
本文目录