题目

力扣题目链接

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

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

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

示例 1:
image.png

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

示例 2:

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

提示:

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

思路

都知道 N 皇后问题是回溯算法解决的经典问题,但是用回溯解决多了组合、切割、子集、排列、行程问题之后,遇到这种二维矩阵还会有点不知所措。

首先来看一下皇后们的约束条件:

  1. 不能同行
  2. 不能同列
  3. 不能同斜线

确定完约束条件,来看看究竟要怎么去搜索皇后们的位置,其实搜索皇后的位置,可以抽象为一棵树。

下面我用一个3 * 3 的棋牌,将搜索过程抽象为一颗树,如图:
image.png
从图中,可以看出,二维矩阵中矩阵的高就是这颗树的高度,矩阵的宽就是树形结构中每一个节点的宽度。

那么我们用皇后们的约束条件,来回溯搜索这颗树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了

回溯三部曲

按照我总结的如下回溯模板,我们来依次分析:

  1. void backtracking(参数) {
  2. if (终止条件) {
  3. 存放结果;
  4. return;
  5. }
  6. for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
  7. 处理节点;
  8. backtracking(路径,选择列表); // 递归
  9. 回溯,撤销处理结果
  10. }
  11. }

1、递归函数参数

我依然是定义全局变量二维数组 result 来记录最终结果。

参数 n 是棋牌的大小,然后用 row 来记录当前遍历到棋盘的第几层了。
代码如下:

  1. int n = 0;
  2. char[][] chessboard = null; // 棋盘
  3. List<List<String>> result = new ArrayList<>(); // 存储结果
  4. void backTracking(int row)

2、递归终止条件

在如下树形结构中:
image.png

可以看出,当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了。

代码如下:

  1. if (row == n) {
  2. // row 是从 0 开始的,因此 row==n 时,说明棋盘的每一行已经遍历完成,此时棋盘的状态即为一种解
  3. result.add(Array2List(chessboard));
  4. return;
  5. }

3、单层搜索的逻辑

递归深度就是 row 控制棋盘的行,每一层里 for 循环的 col 控制棋盘的列,一行一列,确定了放置皇后的位置。

每次都是要从新的一行的起始位置开始搜,所以都是从 0 开始。

代码如下:

  1. for (int col = 0; col < n; ++col) {
  2. if (isValid(row, col)) { // 若该坐标合法
  3. chessboard[row][col] = 'Q'; // 放入皇后
  4. backTracking(row + 1); // 遍历下一行 row+1
  5. chessboard[row][col] = '.'; // 回溯,撤销皇后
  6. }
  7. }

验证棋盘是否合法

按照如下标准去重:

  1. 不能同行
  2. 不能同列
  3. 不能同斜线 (45度和135度角)

代码如下:

  1. // 检查该坐标是否可以放入皇后
  2. public boolean isValid(int row, int col) {
  3. // 检查列
  4. for (int i = 0; i < row; ++i) { // 相当于剪枝
  5. if (chessboard[i][col] == 'Q') {
  6. return false;
  7. }
  8. }
  9. // 检查45度对角线
  10. for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
  11. if (chessboard[i][j] == 'Q') {
  12. return false;
  13. }
  14. }
  15. // 检查135度对角线
  16. for (int i = row - 1, j = col + 1; i >= 0 && j <= n - 1; i--, j++) {
  17. if (chessboard[i][j] == 'Q') {
  18. return false;
  19. }
  20. }
  21. return true;
  22. }

在这份代码中,细心的同学可以发现为什么没有在同行进行检查呢?

因为在单层搜索的过程中,每一层递归,只会选 for 循环(也就是同一行)里的一个元素,所以不用去重了。

那么按照这个模板就不难写出答案了。

总结

本题是我们解决棋盘问题的第一道题目。

如果从来没有接触过N皇后问题的同学看着这样的题会感觉无从下手,可能知道要用回溯法,但也不知道该怎么去搜。

这里我明确给出了棋盘的宽度就是 for 循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法的模板里了

大家可以再仔细体会体会!

答案

Java

  1. class Solution {
  2. int n = 0;
  3. char[][] chessboard = null; // 棋盘
  4. List<List<String>> result = new ArrayList<>(); // 存储结果的 list
  5. public List<List<String>> solveNQueens(int n) {
  6. // 写入全局参数,避免函数参数过多影响可读性
  7. this.n = n;
  8. // 初始化棋盘
  9. chessboard = new char[n][n];
  10. for (char[] c : chessboard) {
  11. Arrays.fill(c, '.');
  12. }
  13. // 棋盘的宽度就是 for 循环的长度,递归的深度就是棋盘的高度,从第一行开始遍历
  14. backTracking(0);
  15. return result;
  16. }
  17. public void backTracking(int row) {
  18. // 终止条件
  19. if (row == n) {
  20. // row 是从 0 开始的,因此 row==n 时,说明棋盘的每一行已经遍历完成,此时棋盘的状态即为一种解
  21. result.add(Array2List(chessboard));
  22. return;
  23. }
  24. // 循环遍历第 row 行的每一个空格,尝试放入皇后
  25. for (int col = 0; col < n; ++col) {
  26. // 若该坐标合法
  27. if (isValid(row, col)) {
  28. // 放入皇后
  29. chessboard[row][col] = 'Q';
  30. // 遍历下一行 row+1
  31. backTracking(row + 1);
  32. // 回溯
  33. chessboard[row][col] = '.';
  34. }
  35. }
  36. }
  37. // 检查该坐标是否可以放入皇后
  38. public boolean isValid(int row, int col) {
  39. // 检查列
  40. for (int i = 0; i < row; ++i) { // 相当于剪枝
  41. if (chessboard[i][col] == 'Q') {
  42. return false;
  43. }
  44. }
  45. // 检查45度对角线
  46. for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
  47. if (chessboard[i][j] == 'Q') {
  48. return false;
  49. }
  50. }
  51. // 检查135度对角线
  52. for (int i = row - 1, j = col + 1; i >= 0 && j <= n - 1; i--, j++) {
  53. if (chessboard[i][j] == 'Q') {
  54. return false;
  55. }
  56. }
  57. return true;
  58. }
  59. // 将棋盘二维数组转换为 List,因为题目要求的数据格式如此
  60. public List Array2List(char[][] chessboard) {
  61. List<String> list = new ArrayList<>();
  62. for (char[] c : chessboard) {
  63. list.add(String.copyValueOf(c));
  64. }
  65. return list;
  66. }
  67. }

REF

https://programmercarl.com/0051.N皇后.html
https://leetcode-cn.com/problems/n-queens/