题目描述

image.png

题目链接

https://leetcode.cn/problems/n-queens/

思路

【51】N 皇后 - 图2皇后问题」研究的是如何将【51】N 皇后 - 图3个皇后放置在【51】N 皇后 - 图4的棋盘上,并且使皇后彼此间不能相互攻击。皇后的走法是:可以横直斜走,格数不限。因此要求皇后彼此间不能相互攻击,等价于要求任何两个皇后都不能在同一行、同一列以及同一条斜线上。

直观的做法是暴力枚举将【51】N 皇后 - 图5个皇后放置在【51】N 皇后 - 图6的棋盘上的所有可能的情况,并对每一种情况判断是否满足皇后彼此之间不相互攻击。但暴力枚举的时间复杂度是非常高的,因此必须利用限制条件加以优化。显然,每个皇后必须位于不同行和不同列,因此将【51】N 皇后 - 图7个皇后放置在【51】N 皇后 - 图8的棋盘上,一定是每一行有且仅有一个皇后,每一列有且仅有一个皇后,且任何两个皇后都不能在同一条斜线上。因此,可以通过回溯的方式寻找可能的解。

回溯的做法是:使用一个数组记录每行放置的皇后的列下标,依次在每一行放置一个皇后。每次新放置的皇后都不能和已经放置的皇后之间有攻击:即新放置的皇后不能和任何一个已经放置的皇后在同一列以及同一条斜线上,并更新数组中的当前行的皇后列下标。当【51】N 皇后 - 图9个皇后都放置完毕,则找到一个可能的解。当找到一个可能的解之后,将数组转换成表示棋盘状态的列表,并将该棋盘状态的列表加入返回列表。
image.png
由于每个皇后必须位于不同列,因此已经放置的皇后所在的列不能放置别的皇后。第一个皇后有【51】N 皇后 - 图11列可选择,第二个皇后最多有【51】N 皇后 - 图12列可选择,第三个皇后最多有【51】N 皇后 - 图13列可选择(如果考虑到不能在同一条斜线上,可能的选择数量更少),因此所有可能的情况不会超过【51】N 皇后 - 图14种,遍历这些情况的时间复杂度是【51】N 皇后 - 图15

为了降低总时间复杂度,每次放置皇后时需要快速判断每个位置是否可以放置皇后,显然,最理想的情况是在【51】N 皇后 - 图16的时间内判断该位置所在的列和两条斜线上是否已经有皇后。我们通过使用「集合」对皇后的放置位置进行判断,这样就可以在【51】N 皇后 - 图17的时间内判断一个位置是否可以放置皇后。

基于集合的回溯
为了判断一个位置所在的列和两条斜线上是否已经有皇后,我们可以使用三个集合【51】N 皇后 - 图18【51】N 皇后 - 图19【51】N 皇后 - 图20分别记录每一列以及两个方向的每条斜线上是否有皇后。

列的表示法很直观,一共有【51】N 皇后 - 图21列,每一列的下标范围从【51】N 皇后 - 图22【51】N 皇后 - 图23,使用列的下标即可明确表示每一列。

如何表示两个方向的斜线呢?对于每个方向的斜线,需要找到斜线上的每个位置的行下标与列下标间的关系。比如从左上到右下的这个方向,同一条斜线上的每个位置满足行下标与列下标之差相等,例如【51】N 皇后 - 图24【51】N 皇后 - 图25就是在该方向的斜线上。因此使用行下标与列下标之差即可明确表示从左上到右下方向的斜线。
image.png
而从右上到左下方向的斜线,同一条斜线上的每个位置满足行下标与列下标之和相等,例如【51】N 皇后 - 图27【51】N 皇后 - 图28就是在该方向的斜线上。因此使用行下标与列下标之和即可明确表示从右上到左下方向的斜线。
image.png
每次放置皇后时,对于每个位置判断其是否在这三个集合中,如果这三个集合都不包含当前位置,则当前位置是可以放置皇后的位置。

代码实现

  1. public List<List<String>> solveNQueens(int n) {
  2. List<List<String>> ans = new ArrayList<>();
  3. // queens用来存储皇后位置
  4. int[] queens = new int[n];
  5. Arrays.fill(queens, -1);
  6. // 回溯
  7. backtrack(ans, queens, n, 0);
  8. return ans;
  9. }
  10. private final Set<Integer> columns = new HashSet<>();
  11. private final Set<Integer> diagonals1 = new HashSet<>();
  12. private final Set<Integer> diagonals2 = new HashSet<>();
  13. private void backtrack(List<List<String>> ans, int[] queens, int n, int row) {
  14. // 遍历完最后一行时,保存当前棋盘
  15. if (row == n) {
  16. List<String> board = generateBoard(queens, n);
  17. ans.add(board);
  18. return;
  19. }
  20. // 遍历当前行的每一列
  21. for (int column = 0; column < n; column++) {
  22. // 过滤不符合放棋子的位置
  23. if (columns.contains(column)) {
  24. continue;
  25. }
  26. int diagonal1 = row - column;
  27. if (diagonals1.contains(diagonal1)) {
  28. continue;
  29. }
  30. int diagonal2 = row + column;
  31. if (diagonals2.contains(diagonal2)) {
  32. continue;
  33. }
  34. // 在当前行的column列处放置一颗棋子
  35. queens[row] = column;
  36. columns.add(column);
  37. diagonals1.add(diagonal1);
  38. diagonals2.add(diagonal2);
  39. // 处理下一行
  40. backtrack(ans, queens, n, row + 1);
  41. // 回溯
  42. queens[row] = -1;
  43. columns.remove(column);
  44. diagonals1.remove(diagonal1);
  45. diagonals2.remove(diagonal2);
  46. }
  47. }
  48. public List<String> generateBoard(int[] queens, int n) {
  49. List<String> board = new ArrayList<>();
  50. for (int i = 0; i < n; i++) {
  51. char[] row = new char[n];
  52. Arrays.fill(row, '.');
  53. row[queens[i]] = 'Q';
  54. board.add(new String(row));
  55. }
  56. return board;
  57. }

复杂度分析

  • 时间复杂度:【51】N 皇后 - 图30,其中【51】N 皇后 - 图31是皇后数量。
  • 空间复杂度:【51】N 皇后 - 图32,其中【51】N 皇后 - 图33是皇后数量。空间复杂度主要取决于递归调用层数、记录每行放置的皇后的列下标的数组以及三个集合,递归调用层数不会超过【51】N 皇后 - 图34,数组的长度为【51】N 皇后 - 图35,每个集合的元素个数也都不会超过【51】N 皇后 - 图36