[USACO1.5] 八皇后 Checker Challenge
按行枚举皇后所在列,用列、主对角线、副对角线三个标记数组 O(1) 判冲突,回溯剪枝求出全部方案并按字典序输出前三个。
OJ: luogu
题目 ID: P1219
难度:普及
标签:DFS回溯剪枝全排列棋盘
日期: 2026-06-19 08:54
形式化题目
有一个
一个方案用长度为
思路
先看一个可以直接验证想法的朴素解:
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-08-13 13:19
* update_at: 2026-08-13 13:19
*/
// brute.cpp:小数据暴力解,把每一行选择的列号看成选择序列,先完整枚举再检查合法性。
// 与 main.cpp 的"边放边剪枝"形成对比,只适合 n <= 10 左右的小数据对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 15;
int n;
int choose[MAXN]; // choose[i]:第 i 行皇后选择的列号(完整的选择序列)
int used_col[MAXN]; // 枚举时保证列号互不重复,形成 n 个列的排列
long long total_ans;
int printed_cnt;
// 检查当前完整选择序列是否合法:任意两行不能在同一条对角线上。
// 列号由 used_col 保证了互不重复,这里只需检查对角线冲突。
bool check() {
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
if (abs(i - j) == abs(choose[i] - choose[j])) {
return false; // 行差等于列差,说明两点在同一条对角线上
}
}
}
return true;
}
void print_solution() {
for (int i = 1; i <= n; i++) {
if (i > 1) {
cout << ' ';
}
cout << choose[i];
}
cout << '\n';
}
// 生成完整选择序列:第 dep 层决定第 dep 行皇后放在哪一列。
// 叶子节点(dep == n + 1)才统一检查合法性并统计答案。
void dfs(int dep) {
if (dep == n + 1) {
if (check()) {
total_ans++;
if (printed_cnt < 3) {
print_solution();
printed_cnt++;
}
}
return;
}
// 这一层从还没用过的列里选一个,保证最终序列是一个排列。
for (int col = 1; col <= n; col++) {
if (used_col[col]) {
continue;
}
choose[dep] = col;
used_col[col] = 1;
dfs(dep + 1);
used_col[col] = 0;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
dfs(1);
cout << total_ans << '\n';
return 0;
}这个暴力把每一行看成一个选择位置:choose[dep] 表示第 dep 行皇后选哪一列,used_col[] 保证列不重复,于是完整枚举的就是 choose[],到叶子节点才用 check() 统一检查对角线冲突,合法就计数,前 3 个直接输出。
这种写法只适合小数据,因为
关键观察:冲突只由三类信息决定
一个位置
- 这一列
col是否已有皇后; - 这一条主对角线(方向
, row - col相同的格子)上是否已有皇后; - 这一条副对角线(方向
, row + col相同的格子)上是否已有皇后。
同一条主对角线上的格子
used_col[col]:第col列是否已占用;used_diag1[row - col + n]:主对角线(加把下标平移到正数)是否已占用; used_diag2[row + col]:副对角线是否已占用。
样例棋盘
这张图展示样例
列: 1 2 3 4 5 6
行 1: . Q . . . .
行 2: . . . Q . .
行 3: . . . . . Q
行 4: Q . . . . .
行 5: . . Q . . .
行 6: . . . . Q .先看"行"和"列":每行每列恰好一个 Q,对应列号互不重复。再看对角线:任意两个 Q 都不在斜线方向上对齐,用公式说就是任意两行
回溯剪枝
从第 row > n 说明得到一个完整方案,总数加一,前 3 个方案直接输出。因为行号从小到大、列号也从小到大,搜索出来的完整序列天然按字典序出现,输出前 3 个就正好满足题意。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-08-13 13:19
* update_at: 2026-08-13 13:19
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 15; // n 最大为 13,多开一点防止越界
const int MAXD = 30; // 对角线编号范围:row - col + n 最大 25,row + col 最大 26
int n;
int pos[MAXN]; // pos[i]:第 i 行皇后所在的列号
int used_col[MAXN]; // used_col[c]:第 c 列是否已放皇后
int used_diag1[MAXD]; // used_diag1[k]:主对角线 row - col + n 是否已放皇后
int used_diag2[MAXD]; // used_diag2[k]:副对角线 row + col 是否已放皇后
long long total_ans; // 合法方案总数
int printed_cnt; // 已经输出的方案个数
// 输出当前完整方案:一行内按行号输出皇后所在列号。
void print_solution() {
for (int i = 1; i <= n; i++) {
if (i > 1) {
cout << ' ';
}
cout << pos[i];
}
cout << '\n';
}
// 从第 row 行开始放皇后,枚举这一行可以放在哪一列。
// 列从小到大枚举,保证完整方案按字典序出现。
void dfs(int row) {
if (row > n) { // 所有行都放完了,得到一个完整方案
total_ans++;
if (printed_cnt < 3) { // 只输出前 3 个解
print_solution();
printed_cnt++;
}
return;
}
for (int col = 1; col <= n; col++) {
int d1 = row - col + n; // 主对角线编号:同一条对角线上 row - col 相等
int d2 = row + col; // 副对角线编号:同一条对角线上 row + col 相等
if (used_col[col] || used_diag1[d1] || used_diag2[d2]) {
continue; // 这一列或某条对角线已被占用,不能放
}
pos[row] = col;
used_col[col] = 1;
used_diag1[d1] = 1;
used_diag2[d2] = 1;
dfs(row + 1);
// 回溯:撤销本行放的皇后
used_col[col] = 0;
used_diag1[d1] = 0;
used_diag2[d2] = 0;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
dfs(1);
cout << total_ans << '\n';
return 0;
}Guide 风格代码
cppbook《C++ 快速入门》教学风格的写法(std:: 前缀、i += 1 循环、0 起始下标):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-08-14 15:18
* update_at: 2026-08-14 15:18
*/
/* P1219 八皇后:按行放皇后,用列、主对角线、副对角线三个标记数组剪枝,回溯求全部方案。 */
#include <iostream>
const int max_n = 14; // n 最大为 13,多开一位
const int max_diag = 30; // 0 起始时对角线编号最大不超过 2 * max_n
int n;
int col_of_row[max_n]; // col_of_row[row]:第 row 行皇后所在的列
int used_col[max_n]; // used_col[col] = true 表示第 col 列已有皇后
int used_diag1[max_diag]; // used_diag1[k] = true 表示主对角线(row - col + n)已有皇后
int used_diag2[max_diag]; // used_diag2[k] = true 表示副对角线(row + col)已有皇后
int total_count = 0; // 合法方案总数
int printed_count = 0; // 已经输出的方案个数
// 输出一个完整方案:第 row 行皇后所在的列号,题目从 1 开始编号
void print_solution() {
for (int row = 0; row < n; row += 1) {
if (row > 0) {
std::cout << ' ';
}
std::cout << col_of_row[row] + 1;
}
std::cout << '\n';
}
// 从第 row 行开始放皇后;列从小到大尝试,保证完整方案按字典序出现
void dfs(int row) {
if (row == n) { // 所有行都放完了,得到一个完整方案
total_count += 1;
if (printed_count < 3) { // 只输出前 3 个方案
print_solution();
printed_count += 1;
}
return;
}
for (int col = 0; col < n; col += 1) {
int d1 = row - col + n; // 主对角线编号:同一条对角线上 row - col 相等
int d2 = row + col; // 副对角线编号:同一条对角线上 row + col 相等
if (used_col[col] || used_diag1[d1] || used_diag2[d2]) {
continue; // 这一列或某条对角线已被占用,不能放
}
col_of_row[row] = col;
used_col[col] = 1;
used_diag1[d1] = 1;
used_diag2[d2] = 1;
dfs(row + 1);
// 回溯:撤销本行放的皇后,让其他分支可以重新使用
used_col[col] = 0;
used_diag1[d1] = 0;
used_diag2[d2] = 0;
}
}
int main() {
std::cin >> n;
dfs(0);
std::cout << total_count << '\n';
return 0;
}复杂度
- 时间复杂度:回溯搜索,最坏为排列规模
,但三类标记把几乎全部非法分支剪在很浅的位置,实际搜索量远小于 ;对 毫秒级完成。 - 空间复杂度:
,包括方案数组 pos[]、三个标记数组和递归栈。
总结
八皇后是回溯搜索的经典模型:把"每个位置能不能放"拆成列、主对角线、副对角线三类互不干扰的约束,分别用数组记录,冲突立即剪枝,回溯时对称撤销。理解
图示解析
这张 ASCII 图展示整道题的解题路线:
暴力:枚举所有列排列,叶子统一检查 (brute.cpp)
choose[dep]:第 dep 行选一列,used_col 保证列不重复
-> 完整生成 n 列排列后 check() 对角线冲突,合法才计数
|
| 瓶颈:n = 13 时叶子 6.2e9 个,合法方案被埋在其中
v
关键观察:冲突只由三类信息决定
列:col 已占用
主对角线:row - col 相同(方向 \)
副对角线:row + col 相同(方向 /)
|
v
三数组回溯剪枝 (main.cpp)
放皇后前 O(1) 查 used_col / used_diag1 / used_diag2
冲突立即剪掉整棵子树,回溯撤销标记
|
v
输出:行号、列号都从小到大枚举
方案天然按字典序出现 -> 前 3 个 + 总数从上往下看:暴力的枚举对象是"完整列排列",它把非法方案也全部生成了一遍,这是慢的根源;中间一行是把冲突拆成三类互不相关的约束,这是能剪枝的根源;最下面一行说明主解把合法性判断提前到放皇后之前,同时列号递增枚举顺便保证了输出顺序。
