题目
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
数据范围
1 <= n <= 9
测试用例
示例1

java
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例2
输入:n = 1
输出:[["Q"]]
题解1(不用看,博主的垃圾代码,虽然过了,但是性能最差,这里只是博主个人记录)
java
class Solution {
List<List<String>> res=new ArrayList<>();
List<String> ans=new ArrayList<>();
char[][] c;
public List<List<String>> solveNQueens(int n) {
c=new char[n][n];
for(int i=0;i<n;i++){
Arrays.fill(c[i],'.');
}
dfs(n,0);
return res;
}
public void dfs(int n,int curr){
if(curr==n){
res.add(new ArrayList<>(ans));
}
for(int i=0;i<n;i++){
if(isOK(curr,i,c,n,curr)){
c[curr][i]='Q';
ans.add(String.valueOf(c[curr]));
dfs(n,curr+1);
c[curr][i]='.';
ans.remove(ans.size()-1);
}
}
}
public boolean isOK(int x,int y,char[][] c,int n,int curr){
for(int i=0;i<curr;i++){
for(int j=0;j<n;j++){
if(c[i][j]=='Q'){
if(i==x||j==y||Math.abs(x-i)==Math.abs(y-j)){
return false;
}
}
}
}
return true;
}
}
题解2(官解1,时间 ON!,空间ON)
java
class Solution {
public List<List<String>> solveNQueens(int n) {
List<List<String>> solutions = new ArrayList<List<String>>();
int[] queens = new int[n];
Arrays.fill(queens, -1);
Set<Integer> columns = new HashSet<Integer>();
Set<Integer> diagonals1 = new HashSet<Integer>();
Set<Integer> diagonals2 = new HashSet<Integer>();
backtrack(solutions, queens, n, 0, columns, diagonals1, diagonals2);
return solutions;
}
public void backtrack(List<List<String>> solutions, int[] queens, int n, int row, Set<Integer> columns, Set<Integer> diagonals1, Set<Integer> diagonals2) {
if (row == n) {
List<String> board = generateBoard(queens, n);
solutions.add(board);
} else {
for (int i = 0; i < n; i++) {
if (columns.contains(i)) {
continue;
}
int diagonal1 = row - i;
if (diagonals1.contains(diagonal1)) {
continue;
}
int diagonal2 = row + i;
if (diagonals2.contains(diagonal2)) {
continue;
}
queens[row] = i;
columns.add(i);
diagonals1.add(diagonal1);
diagonals2.add(diagonal2);
backtrack(solutions, queens, n, row + 1, columns, diagonals1, diagonals2);
queens[row] = -1;
columns.remove(i);
diagonals1.remove(diagonal1);
diagonals2.remove(diagonal2);
}
}
}
public List<String> generateBoard(int[] queens, int n) {
List<String> board = new ArrayList<String>();
for (int i = 0; i < n; i++) {
char[] row = new char[n];
Arrays.fill(row, '.');
row[queens[i]] = 'Q';
board.add(new String(row));
}
return board;
}
}
题解3(官解2,时空同上,但实际空间更好,因为没有试用set)
java
import java.util.*;
class Solution {
// 存放最终所有解的容器
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
// queen数组用于记录每一行皇后放到了哪一列
// 下标是行(row),值是列(col)。例如 queen[0] = 2 表示第0行第2列有皇后
int[] queen = new int[n];
Arrays.fill(queen, -1);
// 初始入口:第0行,三个攻击限制变量(列、左对角、右对角)全为0(无攻击)
dfs(queen, n, 0, 0, 0, 0);
return res;
}
/**
* @param queen 用于生成结果的数组
* @param n 棋盘大小
* @param row 当前正在处理的行号
* @param columns 列限制(1代表被上面行的皇后攻击了)
* @param dial 左对角线限制(1代表被攻击),进入下一行时需要左移
* @param diar 右对角线限制(1代表被攻击),进入下一行时需要右移
*/
public void dfs(int[] queen, int n, int row, int columns, int dial, int diar) {
// --- 递归终止条件 ---
// 如果 row 等于 n,说明 0 ~ n-1 行都成功放好了皇后
if (row == n) {
res.add(construct(queen, n)); // 记录答案
return; // 回溯:返回上一层,继续寻找其他可能性
}
/* * --- 核心逻辑:计算当前行所有能放皇后的位置 ---
* 1. (columns | dial | diar):
* 把三种攻击范围并起来。结果中 1 代表"危险",0 代表"安全"。
* * 2. ~(...):
* 取反。变成 1 代表"安全",0 代表"危险"。
* (注意:取反后,高位会有很多无意义的 1,比如前 28 位都是 1)
* * 3. ((1 << n) - 1):
* 生成掩码。比如 n=4,就是 0000...1111。
* * 4. & 运算:
* 利用掩码把高位的乱七八糟的 1 过滤掉,只保留我们需要的那 n 列。
*/
int available = ((1 << n) - 1) & (~(columns | dial | diar));
// --- 遍历当前行所有可能的解 ---
// 只要 available 不为 0,说明还有位置没试过
while (available != 0) {
// 1. 取出最低位的 1 (Lowbit技巧)
// 这是我们当前决定尝试放置皇后的位置 (例如 00100)
int pos = available & (-available);
// 2. 将最低位的 1 置零
// 表示这个位置我们现在拿去用了,在当前行的后续循环中不要再重复选它
available = (available & (available - 1));
// 3. 将二进制位置转换为具体的列索引 (例如 00100 -> 2)
// 这一步只是为了填 queen 数组生成答案用,不参与核心回溯逻辑
int column = Integer.bitCount(pos - 1);
queen[row] = column;
/* * --- 递归下潜 (Drill Down) ---
* 这里是最精妙的地方,直接在参数里计算下一行的限制:
* * 1. columns | pos:
* 列限制直接传递,当前列被占,下一行同一列也被占。
* * 2. (dial | pos) << 1:
* 左对角线限制。因为到了下一行,左边的攻击范围会往左偏一格,所以左移。
* * 3. (diar | pos) >> 1:
* 右对角线限制。因为到了下一行,右边的攻击范围会往右偏一格,所以右移。
*/
dfs(queen, n, row + 1,
columns | pos,
(dial | pos) << 1,
(diar | pos) >> 1);
// --- 恢复现场 ---
// 虽然 columns/dial/diar 是值传递不需要恢复,但 queen 数组是引用传递
// 需要重置,虽然后续覆盖写入也没问题,但写上比较规范
queen[row] = -1;
}
}
// 辅助函数:把数字索引转成字符串棋盘
public List<String> construct(int[] queen, int n) {
List<String> curr = new ArrayList<>();
for (int i = 0; i < n; i++) {
char[] temp = new char[n];
Arrays.fill(temp, '.');
temp[queen[i]] = 'Q';
curr.add(new String(temp));
}
return curr;
}
}
思路
回溯中可以说最经典的题,N皇后问题,题解1,博主就是使用了N*N的数组记录皇后位置,导致空间变为O(N2),引以为戒。题解2就做出了优化,因为是每行每行处理,我们只用一个一维数组记录位置就好了,然后额外写一个方法来处理返回值。(博主其实一开始也想用一维数组来处理,但是想着不好返回,就使用了二维数组,脑子没转过来可以写一个处理方法)。
这道题的普通回溯解法配不上困难的词条,我们要挑战自己,就得学习位运算,其实位运算的处理方法并没有提升这道题的思维难度。只是在考察你对位运算的熟练度,无论是与或处理来得到可用范围,还是用getcount巧妙获取当前最后一个1的位置,在思维上都很简单,单纯考你位运算的熟练度,大家不要惧怕,熟练这些操作即可。