1. 深度优先搜索算法精解:从经典八皇后到变种问题实战
深度优先搜索(DFS)是算法竞赛和编程面试中的常客,而八皇后问题则是DFS最经典的练手题目。今天我想和大家分享三个基于DFS的皇后类问题变种:2n皇后问题、8皇后·改和棋盘多项式问题。这些题目看似相似,实则各有巧妙之处,特别适合用来训练递归思维和回溯技巧。
2. 2n皇后问题解析与实现
2.1 问题重述与难点分析
2n皇后问题要求在一个n×n的棋盘上放置n个黑皇后和n个白皇后,满足:
- 任意两个同色皇后不在同一行、同一列或同一条对角线
- 棋盘某些位置可能被标记为不可放置(值为0)
- 黑白皇后之间可以互相攻击(题目只限制同色皇后)
这个问题的特殊之处在于需要处理两种皇后的放置顺序和相互影响。我的解决思路是分两步走:先放置黑皇后,再在剩余位置放置白皇后。
2.2 核心算法设计
cpp复制int n;
int board[10][10]; // 棋盘,1可放,0不可放
int ans = 0;
// 黑皇后占用标记
bool colB[10], diag1B[20], diag2B[20];
// 白皇后占用标记
bool colW[10], diag1W[20], diag2W[20];
// 黑皇后位置标记
bool black[10][10];
这里使用了三个关键技巧:
- colB数组标记列是否被黑皇后占用
- diag1B和diag2B分别标记两条对角线
- black数组记录黑皇后位置,供白皇后放置时避开
2.3 分步实现详解
2.3.1 黑皇后放置
cpp复制void dfs_black(int row) {
if (row == n) {
// 黑皇后放置完成后,开始放置白皇后
memset(colW, false, sizeof(colW));
memset(diag1W, false, sizeof(diag1W));
memset(diag2W, false, sizeof(diag2W));
dfs_white(0, 0);
return;
}
for (int col = 0; col < n; col++) {
if (board[row][col] == 1 && !colB[col]
&& !diag1B[row + col] && !diag2B[row - col + n]) {
// 标记占用
colB[col] = diag1B[row + col] = diag2B[row - col + n] = true;
black[row][col] = true;
dfs_black(row + 1); // 递归下一行
// 回溯
black[row][col] = false;
colB[col] = diag1B[row + col] = diag2B[row - col + n] = false;
}
}
}
2.3.2 白皇后放置
cpp复制void dfs_white(int row, int count) {
if (count == n) {
ans++; // 找到一种合法方案
return;
}
if (row >= n) return;
for (int col = 0; col < n; col++) {
if (board[row][col] == 1 && !black[row][col] // 不是黑皇后位置
&& !colW[col] && !diag1W[row + col] && !diag2W[row - col + n]) {
// 标记占用
colW[col] = diag1W[row + col] = diag2W[row - col + n] = true;
dfs_white(row + 1, count + 1);
// 回溯
colW[col] = diag1W[row + col] = diag2W[row - col + n] = false;
}
}
// 尝试不在当前行放置白皇后
dfs_white(row + 1, count);
}
2.4 性能优化与注意事项
- 对角线处理技巧:使用row+col和row-col+n来唯一标识两条对角线
- 回溯时务必恢复所有状态,否则会影响后续搜索
- 由于n≤8,这种DFS实现完全可以在合理时间内完成
- 白皇后放置时,需要跳过已经被黑皇后占据的位置
3. 8皇后·改问题解析
3.1 问题特殊之处
8皇后·改在经典8皇后问题基础上增加了新的挑战:
- 棋盘每个格子有数字权重
- 需要找出所有合法摆放中数字和最大的方案
- 仍然是8×8棋盘,但目标从计数变为求最大值
3.2 算法实现关键
cpp复制int board[8][8];
bool col[8]; // 列占用标记
bool diag1[20]; // 主对角线 row-col+7
bool diag2[20]; // 副对角线 row+col
int ans = 0;
void dfs(int row, int sum) {
if(row == 8) {
ans = max(ans, sum); // 更新最大值
return;
}
for(int c = 0; c < 8; c++) {
if(!col[c] && !diag1[row-c+7] && !diag2[row+c]) {
col[c] = diag1[row-c+7] = diag2[row+c] = true;
dfs(row + 1, sum + board[row][c]); // 累加数字
col[c] = diag1[row-c+7] = diag2[row+c] = false;
}
}
}
3.3 实现技巧
- 逐行放置皇后,确保每行只有一个
- 使用三个数组分别记录列和两条对角线的占用情况
- 递归时传递当前累计的数字和
- 到达最后一行时更新全局最大值
4. 棋盘多项式问题解析
4.1 问题特殊规则
棋盘多项式问题引入了全新的规则:
- 棋盘上有"洞"(值为0),不能放车
- 车的攻击范围被洞阻断
- 需要统计放置k个车(k从0到最大可能)的方案数
4.2 算法实现
cpp复制vector<vector<int>> board; // 棋盘
vector<vector<bool>> hasChess; // 车的位置标记
vector<int> result; // 结果统计
bool canPlace(int x, int y) {
if (board[x][y] == 0) return false;
// 检查四个方向,遇到洞就停止
for (int j = y-1; j >= 0; j--) {
if (board[x][j] == 0) break;
if (hasChess[x][j]) return false;
}
// 其他三个方向检查类似...
return true;
}
void dfs(int idx, int cnt) {
if (idx == n * n) {
result[cnt]++;
return;
}
int x = idx / n, y = idx % n;
// 不放车
dfs(idx + 1, cnt);
// 放车
if (canPlace(x, y)) {
hasChess[x][y] = true;
dfs(idx + 1, cnt + 1);
hasChess[x][y] = false;
}
}
4.3 关键实现细节
- 使用线性索引(idx)遍历整个棋盘,比双重循环更易处理
- canPlace函数需要仔细检查四个方向,遇到洞就停止
- 结果统计数组result记录各k值对应的方案数
- 回溯时需要恢复hasChess标记
5. DFS算法通用优化技巧
通过这三个问题的实践,我总结出一些DFS算法的通用优化技巧:
- 状态压缩:对于小规模棋盘(n≤16),可以用位运算代替数组标记
- 剪枝策略:在递归前评估剩余可能性,提前终止无解路径
- 记忆化:对于重复子问题,可以缓存计算结果
- 迭代加深:对于深度不确定的问题,可以逐步增加搜索深度
- 对称性剪枝:利用棋盘的对称性减少重复计算
6. 常见错误与调试技巧
在实现这类DFS算法时,容易遇到以下问题:
- 忘记回溯:这是最常见的错误,务必在递归返回后恢复所有状态
- 边界条件错误:特别是棋盘边缘的位置检查
- 对角线计算错误:确保row+col和row-col的偏移量计算正确
- 递归终止条件不完整:可能导致无限递归或漏解
- 全局变量未重置:在多次测试时会产生干扰
调试时可以:
- 打印中间状态,观察递归过程
- 对小规模测试用例手动验证
- 使用条件断点跟踪特定位置的放置情况
- 编写检查函数验证当前状态的合法性
7. 算法复杂度分析
- 2n皇后问题:最坏情况O((n!)^2),但实际有剪枝会好很多
- 8皇后·改:经典8皇后问题有92个解,复杂度类似
- 棋盘多项式:最坏O(2^(n^2)),但实际有洞会减少很多情况
对于n≤8的问题规模,这些算法都是可行的。当n增大时,需要考虑更高效的算法或启发式方法。
8. 扩展思考与练习题
如果想进一步挑战自己,可以尝试以下变种问题:
- n皇后问题的所有解可视化
- 加入皇后移动规则,求最少步数达到目标状态
- 三维棋盘上的皇后放置问题
- 不同棋子组合的放置问题(如皇后+车+象)
- 大规模棋盘的近似解法
对于初学者,我建议按照以下顺序练习:
- 经典8皇后问题
- 2n皇后问题
- 带权重的8皇后
- 棋盘多项式问题
- 其他更复杂的变种
记住,掌握DFS的关键是多练习,理解递归的本质,并熟练运用回溯技巧。这三个问题提供了很好的训练机会,建议自己动手实现一遍,遇到问题时再参考这里的解决方案。