把 n 个皇后放到 n × n 的棋盘上,使任意两个皇后都不在同一行、同一列、同一条对角线上,返回所有不同的摆法(每种摆法用 n 个字符串表示棋盘,Q 是皇后,. 是空位)。做法是回溯:一行放一个皇后,逐行往下放,每放一个之前检查它所在的列和两条对角线有没有被占。判断对角线有一个巧妙的编码:同一条"主对角线"(\ 方向)上的格子 r - c 相同,同一条"副对角线"(/ 方向)上的格子 r + c 相同。本文用真实数据展示漏掉一条对角线会多出多少"解",实测回溯与枚举全部排列的耗时,给出五种语言的实现,并把每一步的放置和冲突做成分步演示。
完整题目见官方页面(链接在文末)。摆法的先后顺序不作要求,按集合比较。基准解枚举 0 到 n-1 的全部排列(每行一个皇后,排列决定了皇后所在的列,这样行和列自动不冲突),只检查对角线,是与回溯完全不同的写法。一共 8 组用例,n 从 1 到 8:
输入 [1] 输出 [["Q"]] 输入 [2] 输出 [] 输入 [3] 输出 [] 输入 [4] 输出 [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] 输入 [5] 输出 [["Q....","..Q..","....Q",".Q...","...Q."],["Q....","...Q.",".Q...","....Q","..Q.."],[".Q...","...Q.","Q....","..Q..","....Q"],[".Q...","....Q","..Q..","Q....","...Q."],["..Q..","Q....","...Q.",".Q...","....Q"],["..Q..","....Q",".Q...","...Q.","Q...."],["...Q.","Q....","..Q..","....Q",".Q..."],["...Q.",".Q...","....Q","..Q..","Q...."],["....Q",".Q...","...Q.","Q....","..Q.."],["....Q","..Q..","Q....","...Q.",".Q..."]] 输入 [6] 输出 [[".Q....","...Q..",".....Q","Q.....","..Q...","....Q."],["..Q...",".....Q",".Q....","....Q.","Q.....","...Q.."],["...Q..","Q.....","....Q.",".Q....",".....Q","..Q..."],["....Q.","..Q...","Q.....",".....Q","...Q..",".Q...."]]
下面是 n 从 1 到 9 时本文解法得到的解的个数,以及一个漏掉副对角线(只检查列和主对角线)的写法得到的个数:
n | 解的个数(solve)| 漏掉副对角线的写法得到的个数 1 | 1 | 1 2 | 0 | 1 3 | 0 | 3 4 | 2 | 7 5 | 10 | 23 6 | 4 | 83 7 | 40 | 405 8 | 92 | 2113 9 | 352 | 12657
正确的个数依次是 1、0、0、2、10、4、40、92、352(n = 2 和 n = 3 没有解),与文献里已知的 N 皇后解的个数一致(n = 8 是 92,这是这个问题最有名的一个数)。漏掉一条对角线的写法给出的数远大于正确值:n = 8 时是 2113 而不是 92,n = 2 时甚至认为有 1 个解,而实际上一个都没有。这类错误在小棋盘上会碰巧对(n = 1),棋盘大一点就暴露了。
用三个集合(或布尔数组)记录被占用的列、主对角线(r - c,Java、Go、JavaScript、C++ 里加上 n - 1 使下标非负)、副对角线(r + c)。dfs(r) 尝试在第 r 行的每一列放皇后:三处都没被占才放;放完递归到下一行;返回时撤销。放满 n 行就把 queens(每行皇后所在的列)翻译成 n 个字符串收集起来。Python、Java、Go、JavaScript、C++ 都是真实编译运行的。
Python · 解法代码def solve(n): res = [] cols, diag1, diag2 = set(), set(), set() # 已经被占用的列、主对角线(r-c 相同)、副对角线(r+c 相同) queens = [] # queens[r] = 第 r 行皇后所在的列 def dfs(r): if r == n: res.append(["." * c + "Q" + "." * (n - c - 1) for c in queens]) return for c in range(n): if c in cols or r - c in diag1 or r + c in diag2: continue # 同列或同对角线:会互相攻击 cols.add(c) diag1.add(r - c) diag2.add(r + c) queens.append(c) dfs(r + 1) queens.pop() # 回溯:撤销这一行的皇后 cols.remove(c) diag1.remove(r - c) diag2.remove(r + c) dfs(0) return res
$ python3 solution.py # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON [1] → [["Q"]] [2] → [] [3] → [] [4] → [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] [5] → [["Q....","..Q..","....Q",".Q...","...Q."],["Q....","...Q.",".Q...","....Q","..Q.."],[".Q...","...Q.","Q....","..Q..","....Q"],[".Q...","....Q","..Q..","Q....","...Q."],["..Q..","Q....","...Q.",".Q...","....Q"],["..Q..","....Q",".Q...","...Q.","Q...."],["...Q.","Q....","..Q..","....Q",".Q..."],["...Q.",".Q...","....Q","..Q..","Q...."],["....Q",".Q...","...Q.","Q....","..Q.."],["....Q","..Q..","Q....","...Q.",".Q..."]] [6] → [[".Q....","...Q..",".....Q","Q.....","..Q...","....Q."],["..Q...",".....Q",".Q....","....Q.","Q.....","...Q.."],["...Q..","Q.....","....Q.",".Q....",".....Q","..Q..."],["....Q.","..Q...","Q.....",".....Q","...Q..",".Q...."]]
Java · 解法代码class Solution { private int n; private boolean[] cols, diag1, diag2; // 已占用的列、主对角线(r-c+n-1)、副对角线(r+c) private int[] queens; // queens[r] = 第 r 行皇后所在的列 private List<List<String>> res; public List<List<String>> solveNQueens(int n) { this.n = n; cols = new boolean[n]; diag1 = new boolean[2 * n]; diag2 = new boolean[2 * n]; queens = new int[n]; res = new ArrayList<>(); dfs(0); return res; } private void dfs(int r) { if (r == n) { List<String> board = new ArrayList<>(); for (int c : queens) { char[] row = new char[n]; Arrays.fill(row, '.'); row[c] = 'Q'; board.add(new String(row)); } res.add(board); return; } for (int c = 0; c < n; c++) { if (cols[c] || diag1[r - c + n - 1] || diag2[r + c]) continue; // 同列或同对角线:会互相攻击 cols[c] = diag1[r - c + n - 1] = diag2[r + c] = true; queens[r] = c; dfs(r + 1); cols[c] = diag1[r - c + n - 1] = diag2[r + c] = false; // 回溯:撤销这一行的皇后 } } }
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl [1] → [["Q"]] [2] → [] [3] → [] [4] → [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] [5] → [["Q....","..Q..","....Q",".Q...","...Q."],["Q....","...Q.",".Q...","....Q","..Q.."],[".Q...","...Q.","Q....","..Q..","....Q"],[".Q...","....Q","..Q..","Q....","...Q."],["..Q..","Q....","...Q.",".Q...","....Q"],["..Q..","....Q",".Q...","...Q.","Q...."],["...Q.","Q....","..Q..","....Q",".Q..."],["...Q.",".Q...","....Q","..Q..","Q...."],["....Q",".Q...","...Q.","Q....","..Q.."],["....Q","..Q..","Q....","...Q.",".Q..."]] [6] → [[".Q....","...Q..",".....Q","Q.....","..Q...","....Q."],["..Q...",".....Q",".Q....","....Q.","Q.....","...Q.."],["...Q..","Q.....","....Q.",".Q....",".....Q","..Q..."],["....Q.","..Q...","Q.....",".....Q","...Q..",".Q...."]]
Go · 解法代码func solveNQueens(n int) [][]string { res := [][]string{} cols := make([]bool, n) diag1 := make([]bool, 2*n) // 主对角线:r-c+n-1 相同 diag2 := make([]bool, 2*n) // 副对角线:r+c 相同 queens := make([]int, n) // queens[r] = 第 r 行皇后所在的列 var dfs func(r int) dfs = func(r int) { if r == n { board := make([]string, n) for i, c := range queens { board[i] = strings.Repeat(".", c) + "Q" + strings.Repeat(".", n-c-1) } res = append(res, board) return } for c := 0; c < n; c++ { if cols[c] || diag1[r-c+n-1] || diag2[r+c] { // 同列或同对角线:会互相攻击 continue } cols[c], diag1[r-c+n-1], diag2[r+c] = true, true, true queens[r] = c dfs(r + 1) cols[c], diag1[r-c+n-1], diag2[r+c] = false, false, false // 回溯:撤销这一行的皇后 } } dfs(0) return res }
$ go build -o bin . && ./bin < cases.jsonl [1] → [["Q"]] [2] → [] [3] → [] [4] → [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] [5] → [["Q....","..Q..","....Q",".Q...","...Q."],["Q....","...Q.",".Q...","....Q","..Q.."],[".Q...","...Q.","Q....","..Q..","....Q"],[".Q...","....Q","..Q..","Q....","...Q."],["..Q..","Q....","...Q.",".Q...","....Q"],["..Q..","....Q",".Q...","...Q.","Q...."],["...Q.","Q....","..Q..","....Q",".Q..."],["...Q.",".Q...","....Q","..Q..","Q...."],["....Q",".Q...","...Q.","Q....","..Q.."],["....Q","..Q..","Q....","...Q.",".Q..."]] [6] → [[".Q....","...Q..",".....Q","Q.....","..Q...","....Q."],["..Q...",".....Q",".Q....","....Q.","Q.....","...Q.."],["...Q..","Q.....","....Q.",".Q....",".....Q","..Q..."],["....Q.","..Q...","Q.....",".....Q","...Q..",".Q...."]]
JavaScript · 解法代码function solveNQueens(n) { const res = []; const cols = new Array(n).fill(false); const diag1 = new Array(2 * n).fill(false); // 主对角线:r-c+n-1 相同 const diag2 = new Array(2 * n).fill(false); // 副对角线:r+c 相同 const queens = new Array(n).fill(0); // queens[r] = 第 r 行皇后所在的列 const dfs = (r) => { if (r === n) { res.push(queens.map((c) => ".".repeat(c) + "Q" + ".".repeat(n - c - 1))); return; } for (let c = 0; c < n; c++) { if (cols[c] || diag1[r - c + n - 1] || diag2[r + c]) continue; // 同列或同对角线:会互相攻击 cols[c] = diag1[r - c + n - 1] = diag2[r + c] = true; queens[r] = c; dfs(r + 1); cols[c] = diag1[r - c + n - 1] = diag2[r + c] = false; // 回溯:撤销这一行的皇后 } }; dfs(0); return res; }
$ node run.js < cases.jsonl [1] → [["Q"]] [2] → [] [3] → [] [4] → [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] [5] → [["Q....","..Q..","....Q",".Q...","...Q."],["Q....","...Q.",".Q...","....Q","..Q.."],[".Q...","...Q.","Q....","..Q..","....Q"],[".Q...","....Q","..Q..","Q....","...Q."],["..Q..","Q....","...Q.",".Q...","....Q"],["..Q..","....Q",".Q...","...Q.","Q...."],["...Q.","Q....","..Q..","....Q",".Q..."],["...Q.",".Q...","....Q","..Q..","Q...."],["....Q",".Q...","...Q.","Q....","..Q.."],["....Q","..Q..","Q....","...Q.",".Q..."]] [6] → [[".Q....","...Q..",".....Q","Q.....","..Q...","....Q."],["..Q...",".....Q",".Q....","....Q.","Q.....","...Q.."],["...Q..","Q.....","....Q.",".Q....",".....Q","..Q..."],["....Q.","..Q...","Q.....",".....Q","...Q..",".Q...."]]
C++ · 解法代码#include <string> #include <vector> using namespace std; class Solution { int n; vector<bool> cols, diag1, diag2; // 已占用的列、主对角线(r-c+n-1)、副对角线(r+c) vector<int> queens; // queens[r] = 第 r 行皇后所在的列 vector<vector<string>> res; void dfs(int r) { if (r == n) { vector<string> board; for (int c : queens) { string row(n, '.'); row[c] = 'Q'; board.push_back(row); } res.push_back(board); return; } for (int c = 0; c < n; c++) { if (cols[c] || diag1[r - c + n - 1] || diag2[r + c]) continue; // 同列或同对角线:会互相攻击 cols[c] = diag1[r - c + n - 1] = diag2[r + c] = true; queens[r] = c; dfs(r + 1); cols[c] = diag1[r - c + n - 1] = diag2[r + c] = false; // 回溯:撤销这一行的皇后 } } public: vector<vector<string>> solveNQueens(int n) { this->n = n; cols.assign(n, false); diag1.assign(2 * n, false); diag2.assign(2 * n, false); queens.assign(n, 0); dfs(0); return res; } };
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl [1] → [["Q"]] [2] → [] [3] → [] [4] → [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] [5] → [["Q....","..Q..","....Q",".Q...","...Q."],["Q....","...Q.",".Q...","....Q","..Q.."],[".Q...","...Q.","Q....","..Q..","....Q"],[".Q...","....Q","..Q..","Q....","...Q."],["..Q..","Q....","...Q.",".Q...","....Q"],["..Q..","....Q",".Q...","...Q.","Q...."],["...Q.","Q....","..Q..","....Q",".Q..."],["...Q.",".Q...","....Q","..Q..","Q...."],["....Q",".Q...","...Q.","Q....","..Q.."],["....Q","..Q..","Q....","...Q.",".Q..."]] [6] → [[".Q....","...Q..",".....Q","Q.....","..Q...","....Q."],["..Q...",".....Q",".Q....","....Q.","Q.....","...Q.."],["...Q..","Q.....","....Q.",".Q....",".....Q","..Q..."],["....Q.","..Q...","Q.....",".....Q","...Q..",".Q...."]]
共 8 组用例(含边界),以暴力解的结果为基准逐组比对: Python 通过 8/8 Java 通过 8/8 Go 通过 8/8 JavaScript 通过 8/8 C++ 通过 8/8
n 分别为 4、6、8、9,每个规模取三次中最快的一次,基准解是枚举全部 n! 个排列后检查对角线:
n | 暴力解 | 本文解法
4 | 0.010 ms | 0.008 ms
6 | 0.232 ms | 0.092 ms
8 | 14.410 ms | 1.522 ms
9 | (未测,太慢) | 6.493 msn = 8 时,回溯 1.52ms,枚举排列 14.41ms,约 9.5 倍(枚举要检查 40320 个排列,回溯提前剪掉了大部分)。回溯从 n = 8 到 n = 9 增长约 4.3 倍,n 更大时会继续快速增长,所以这道题的 n 只到 9。基准在 n = 9 时没有再测。
Python 版本另外对 n 从 1 到 7 的随机取值和枚举排列做了对拍:
随机对拍 50 组(n 取 1 到 7 的随机值),solve 与 brute 的结果全部一致。
n 只有 1 到 8 这几个值,固定用例已经覆盖了全部小规模输入。另外四种语言只在上面 8 组固定用例上验证过,其中包含了每个 n 的完整解集。
下面的演示用 n = 4,共 2 个解。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示最终得到的解集与 solve 的返回值一致。琥珀色是已经放下的皇后,蓝色是正在尝试的位置。注意"同列""同主对角线""同副对角线"三种冲突各自的提示,以及最后几行"回到上一行换一个位置"的回溯帧。