LeetCode 52:N皇后 II
问题本质与核心目标
N皇后 II 要求计算在 N×N
棋盘上放置 N
个皇后的合法方案数(皇后间互不攻击,即同一行、列、对角线无重复)。与第51题(输出所有布局)不同,本题只需计数,因此可通过 回溯法 + 冲突标记优化 高效求解。
核心思路:回溯 + 冲突标记数组
1. 冲突判断的优化
皇后冲突分为三类:列冲突、主对角线冲突、副对角线冲突。通过三个布尔数组快速标记:
- 列冲突:
cols[col]
→ 标记列col
是否已有皇后。 - 主对角线冲突:主对角线上的点满足
row - col = 常数
,转换为非负索引row - col + (n-1)
(避免负数),用diag1[]
标记。 - 副对角线冲突:副对角线上的点满足
row + col = 常数
,用diag2[]
标记。
2. 回溯流程
- 逐行放置:从第0行开始,尝试当前行的每一列。
- 冲突检查:利用三个标记数组,
O(1)
时间判断当前列是否可放置皇后。 - 递归与回溯:若可放置,标记冲突并递归处理下一行;递归返回后,回溯(撤销标记)以尝试其他列。
- 计数触发:当处理完所有行(
row == n
),说明找到一种合法方案,计数加一。
算法步骤详解
步骤 1:初始化标记数组与计数
class Solution {
private int count = 0; // 合法方案数(类成员,方便递归修改)
public int totalNQueens(int n) {
boolean[] cols = new boolean[n]; // 列冲突标记
boolean[] diag1 = new boolean[2 * n - 1]; // 主对角线冲突标记
boolean[] diag2 = new boolean[2 * n - 1]; // 副对角线冲突标记
backtrack(0, n, cols, diag1, diag2); // 从第0行开始回溯
return count;
}
}
步骤 2:回溯函数 backtrack
private void backtrack(int row, int n, boolean[] cols, boolean[] diag1, boolean[] diag2) {
// 终止条件:所有行已放置皇后,找到一种合法方案
if (row == n) {
count++;
return;
}
// 遍历当前行的所有列(0 ~ n-1)
for (int col = 0; col < n; col++) {
// 计算主、副对角线的索引(转换为非负)
int d1 = row - col + n - 1; // 主对角线索引:避免负数,范围 [0, 2n-2]
int d2 = row + col; // 副对角线索引:范围 [0, 2n-2]
// 检查列、主对角线、副对角线是否均未被占用
if (!cols[col] && !diag1[d1] && !diag2[d2]) {
// 放置皇后:标记冲突
cols[col] = true;
diag1[d1] = true;
diag2[d2] = true;
// 递归处理下一行
backtrack(row + 1, n, cols, diag1, diag2);
// 回溯:撤销标记,尝试下一个列
cols[col] = false;
diag1[d1] = false;
diag2[d2] = false;
}
}
}
关键逻辑解析
1. 对角线索引的数学推导
-
主对角线:
对于点(row, col)
,主对角线的特征是row - col
为定值。由于row
和col
范围是[0, n-1]
,row - col
的范围是[-(n-1), n-1]
。为避免负数索引,加上n-1
,得到[0, 2n-2]
,对应数组diag1
的长度2n-1
。 -
副对角线:
副对角线的特征是row + col
为定值,范围是[0, 2n-2]
,对应数组diag2
的长度2n-1
。
2. 回溯的效率优化
- 冲突检查:通过三个布尔数组,将每次冲突检查的时间从
O(n)
(遍历前面行)降为O(1)
,大幅减少无效递归。 - 空间复用:标记数组在递归过程中被反复修改(标记→回溯→标记),空间复杂度仅为
O(n)
(远低于存储所有布局的方案)。
3. 递归流程示例(以 n=4
为例)
- 初始状态:
row=0
,cols
、diag1
、diag2
全为false
。 - 处理
row=0
:- 尝试
col=0
:- 计算
d1=0-0+3=3
,d2=0+0=0
,三者均为false
→ 标记为true
,递归到row=1
。
- 计算
- 在
row=1
,尝试col=2
(避开已标记的列和对角线),标记后递归到row=2
。 - 最终当
row=4
(等于n=4
)时,count
加1。
- 尝试
- 回溯:递归返回后,撤销标记,继续尝试当前行的下一个列,直到所有可能的列遍历完毕。
完整代码(Java)
class Solution {
private int count = 0; // 记录合法方案的数量
public int totalNQueens(int n) {
// 初始化三个标记数组:列、主对角线、副对角线
boolean[] cols = new boolean[n]; // 列是否被占用
boolean[] diag1 = new boolean[2 * n - 1]; // 主对角线标记,索引为 row - col + n - 1
boolean[] diag2 = new boolean[2 * n - 1]; // 副对角线标记,索引为 row + col
// 从第0行开始回溯
backtrack(0, n, cols, diag1, diag2);
return count;
}
/**
* 回溯函数:尝试在第row行放置皇后
* @param row 当前处理的行
* @param n 皇后数量(棋盘大小)
* @param cols 列占用标记数组
* @param diag1 主对角线占用标记数组
* @param diag2 副对角线占用标记数组
*/
private void backtrack(int row, int n, boolean[] cols, boolean[] diag1, boolean[] diag2) {
// 终止条件:所有行都已成功放置皇后
if (row == n) {
count++; // 找到一种合法方案,计数加1
return;
}
// 遍历当前行的每一列,尝试放置皇后
for (int col = 0; col < n; col++) {
// 计算当前位置对应的主、副对角线索引
int d1 = row - col + n - 1; // 主对角线索引(避免负数,范围0~2n-2)
int d2 = row + col; // 副对角线索引(范围0~2n-2)
// 检查列、主对角线、副对角线是否都未被占用
if (!cols[col] && !diag1[d1] && !diag2[d2]) {
// 放置皇后:标记列和对角线为已占用
cols[col] = true;
diag1[d1] = true;
diag2[d2] = true;
// 递归处理下一行
backtrack(row + 1, n, cols, diag1, diag2);
// 回溯:撤销标记,尝试下一个列
cols[col] = false;
diag1[d1] = false;
diag2[d2] = false;
}
}
}
}
复杂度分析
- 时间复杂度:最坏情况下接近
O(n!)
(无剪枝时),但通过冲突标记的剪枝,实际远低于O(n!)
(例如n=9
时仍可高效运行)。 - 空间复杂度:
O(n)
,三个标记数组的长度分别为n
、2n-1
、2n-1
(均为O(n)
级别),递归栈深度为n
。
该方法通过 回溯 + 冲突标记数组,将冲突检查优化到 O(1)
,在保证正确性的同时大幅提升效率,是解决N皇后计数问题的最优方案。核心思想可推广到类似的组合优化问题(如排列、棋盘覆盖等),体现了回溯算法与状态剪枝的强大结合。