八皇后问题是回溯算法的经典案例,但普通解法在冲突判断时会做大量冗余计算。本文分享一种位运算优化的回溯解法:用二进制掩码替代数组判断冲突,代码更简洁、效率直接起飞,还能直观打印所有解的棋盘!
一、八皇后问题的核心约束
要在8×8棋盘放8个皇后,需满足3条规则:
1. 同列不冲突:每列只能有1个皇后;
2. 左对角线不冲突:行差的绝对值 ≠ 列差的绝对值;
3. 右对角线不冲突:同理左对角线。
二、位运算优化的核心思路
位运算的精髓是用二进制数的“位”表示“列/对角线的占用状态”,通过位操作快速判断冲突、选择可用列。
以8皇后为例,我们用3个整数(二进制掩码)记录状态:
• colMask:列掩码,某一位为1表示该列已被占用;
• leftDiagMask:左对角线掩码,某一位为1表示该左对角线已被占用;
• rightDiagMask:右对角线掩码,某一位为1表示该右对角线已被占用。
三、完整代码实现(C++)
#include <iostream>
using namespace std;
const int N = 8; // 棋盘大小
int solutionCount = 0; // 解的数量
int queens[N]; // 记录皇后位置(queens[i] = j 表示第i行第j列)
/**
* 位运算回溯+记录皇后位置
* @param row 当前行
* @param colMask 列掩码
* @param leftDiagMask 左对角线掩码
* @param rightDiagMask 右对角线掩码
*/
void backtrack(int row, int colMask, int leftDiagMask, int rightDiagMask) {
// 递归终止:所有行都放好皇后,打印当前解
if (row == N) {
cout << "第 " << ++solutionCount << " 种解:\n";
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
cout << (queens[i] == j ? "Q " : ". ");
}
cout << endl;
}
cout << endl;
return;
}
// 计算当前可用列:取反后与“全1掩码”相交,得到可用位为1的数
int availableCols = ~(colMask | leftDiagMask | rightDiagMask) & ((1 << N) - 1);
while (availableCols) {
// 提取最右边的可用列(二进制位)
int colBit = availableCols & -availableCols;
// 把二进制位转换为列号(比如0b100对应第2列)
int col = __builtin_ctz(colBit);
queens[row] = col; // 记录当前行皇后的列号
// 递归下一行:更新掩码
backtrack(
row + 1,
colMask | colBit, // 列掩码:标记当前列已占用
(leftDiagMask | colBit) << 1, // 左对角线掩码:左移1位
(rightDiagMask | colBit) >> 1 // 右对角线掩码:右移1位
);
// 清除当前尝试的列(继续尝试其他可用列)
availableCols &= availableCols - 1;
}
}
int main() {
backtrack(0, 0, 0, 0);
cout << "八皇后问题共有 " << solutionCount << " 种解\n";
return 0;
}
四、关键代码解析
1. 可用列计算
int availableCols = ~(colMask | leftDiagMask | rightDiagMask) & ((1 << N) - 1);
• 把“列/左对角线/右对角线”的占用位合并(colMask | leftDiagMask | rightDiagMask);
• 取反后,用(1 << N) - 1(8位全1,即0b11111111)“截取”低8位,得到当前行可放皇后的列(对应位为1)。
2. 提取最右可用列
int colBit = availableCols & -availableCols;
利用补码特性,快速提取availableCols最右边的1(比如availableCols=0b1010,结果为0b0010)。
3. 列号转换
int col = __builtin_ctz(colBit);
__builtin_ctz是GCC内置函数,返回“二进制数末尾0的个数”:
• 比如colBit=0b100(十进制4),末尾有2个0,对应第2列(从0开始计数)。
4. 掩码更新
递归下一行时,需要更新3个掩码:
• 列掩码:colMask | colBit → 标记当前列已占用;
• 左对角线掩码:(leftDiagMask | colBit) << 1 → 下一行的左对角线是当前左对角线左移1位;
• 右对角线掩码:(rightDiagMask | colBit) >> 1 → 下一行的右对角线是当前右对角线右移1位。
五、运行效果
程序会输出8皇后的所有92种解,每种解以棋盘形式展示(Q表示皇后,.表示空位),例如其中一种解:
第 1 种解:
Q . . . . . . .
. . . . Q . . .
. . . . . . . Q
. . . . . Q . .
. . Q . . . . .
. . . . . . Q .
. Q . . . . . .
. . . Q . . . .
六、为什么位运算更快?
普通回溯法需要用数组遍历判断冲突(时间复杂度O(n)),而位运算通过二进制位操作(与、或、非、移位)实现冲突判断,操作是O(1)时间,且CPU对二进制运算的支持极高效,能大幅减少计算量。

33

被折叠的 条评论
为什么被折叠?



