n皇后问题
描述
n皇后问题:一个n×n的棋盘,在棋盘上摆n个皇后,满足任意两个皇后不能在同一行、同一列或同一斜线上的方案有多少种?
输入
第一行包含一个整数n。
输出
输出一个整数,表示方案数。
样例输入
4
样例输出
2
限制
一共10个测试点, 第i个测试点的n=i+4。
时间:2 sec
空间:512 MB
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
思路
目前这个问题只能用搜索解决,从第一行开始,不断地尝试在不同位置摆放皇后,观察哪些摆放方案是合法的。问题的难点是,如果我们从第 0 行开始依次在每行尝试放皇后,搜索时如何能够方便的记录前面各行放置皇后的位置,以便能快速知道这一行哪些位置是可以放置皇后的,而且当某行没有位置可以放皇后时可以提前结束这次搜索,回退到上一行尝试别的位置。规定棋盘的形状如下图所示。
我们定义以下变量帮助我们记录哪些位置可以放皇后:
· pos: 低n位(0 ~ n-1)中的第i位为1表示该列已经存在一个皇后。
· left: 低n位(0 ~ n-1)中的第i位为1表示该列因为有一条从棋盘左上到右下的对角线被某一皇后占据而不能再放皇后。
· right: 低n位(0 ~ n-1)中的第i位为1表示该列因为有一条从棋盘右上到左下的对角线被某一皇后占据而不能再放皇后。
· allOne:低n位(0 ~ n-1)均为1,其他位均为0的整数,作为掩码,来获得 pos, left, right 的低 n 位。
· can_put:低n位(0 ~ n-1)中的第i位为1表示该位置可以放一个皇后。can_put = allOne & ~(pos | left | right);
算法基本流程如下:
① 判断当前棋盘上是否已经有 n 个皇后。 若是,则结束,得到一种合法方案。 若不是,则尝试在新的一行放一个皇后。 ② 通过 pos, left, right 计算 can_put,若 can_put 第 i 位为 1,则表示该行的第 i 列可以放一个皇后(i = 0, 1, ..., n),这是为了后面查找能放皇后的位置(即找为 1 的位方便)。
can_put = allOne & ~(pos | left | right) // 因为 pos, left, right 为 1 的位表示不能放皇后,而 can_put 为 0 的位表示不能放皇后,故要取反。
// 用 allOne 保证除了低 n 位,更高位没有1。
③ 通过 can_put 获取可以放置皇后的位置,即查找 can_put 中为 1 的位。
这里获取 can_put 中为 1 的位采用如下方法:每次读取 can_put 中最低位的 1,这可以通过 can_put & -can_put 实现。因为整数在计算机中用二进制补码存储,一个整数 d 的相反数的补码可以通过对 d 的
补码按位取反,然后加 1 来实现。这样的结果就是,-d 的补码表示为 d 中的最低为 1 的位及其后的位保持不变,更高位都取反。例如,用 8 位二进制补码表示 [d]补 = 01100100,[-d]补 = 10011100,可以看到,
[-d]补 与 [d]补 最低位的 1 及其后各位相同(其后各位均为 0),其余各位取反。因此,can_put & -can_put 得到一个二进制数,其只有 can_put 的最低为 1 的位为 1,其余各位均为 0。将该数命名为 put,
表示这次放置皇后的位置。
④ 对每一个可以放置皇后的位置放置一个皇后,更新 pos, left, right,然后尝试摆放下一行。
我们按照上述方法计算 put = can_put & -can_put,得到 can_put 最低位的 1,然后尝试在此位置放置一个皇后,然后继续摆放下一行,直至得到一个合法方案或因无法摆放更多皇后而提前终止(can_put = 0)。
以上通过将上述各步封装成函数然后递归来实现。
注意,尝试在下一行摆放皇后时要更新 pos, left, right。更新的方法很简单,由于 put 记录了这次的摆放皇后的位置,pos = pos | put,而 left 和 right 是对角线,到下一行会相应的左或右移一个位置,
参考上图所示棋盘的形状,即可得到 left = (left | put) >> 1, right = (right | put) << 1。
当一次探索结束回到当前位置时,将 can_put 我们刚才尝试的位置为 0(表示该位已经尝试过,不能再放皇后),这通过 can_put = can_put ^ put 来实现,因为 put 只有我们刚才尝试的位为 1,其他位为0,一
个二进制位与 1 异或取反,与 0 异或不变,因此这样就把 can_put 我们刚才尝试的位置为 0 了。下次再读取 can_put 最低位的 1 时就会得到离这步读取最近的更高位的 1 了。然后回到第 ③ 步。
C++代码
#include <iostream> using namespace std; int ans, allOne; // ans:答案;allOne:用于二进制&的全1数。 /* 深度优先搜索(用二进制优化)获得合法的皇后摆放位置。 pos:其二进制上的某个位置的1表示当前所在行的相应的位置(列)已经放了一个皇后。 left:其二进制上的某个位置的1表示当前所在行的相应的位置(是由于右对角线上已有皇后)不能放置皇后。 right:其二进制上的某个位置的1表示当前所在行的相应位置(是由于左对角线上已有皇后)不能放置皇后。 */ void dfs(int pos, int left, int right) { /* 当且仅当每一列都放了一个皇后那么整个棋盘已经放了n个合法皇后,故要终止 */ if ( pos == allOne ) { ++ans; // 到了此步,证明已经得到了一个合法的方案。 return; } /* can_put为1的位表示能放皇后。用掩码保证can_put除了低n位以外的更高位均为0,因为取反和下面循环中的左移可能使除低n位外的更高位出现1. */ int can_put = allOne & ( ~(pos | left | right) ); /* 对于can_put每个为1的位,放置一个皇后,更新pos, left, right,然后继续下一步搜索。 */ while ( can_put ) // 只要put不为0,证明它还有为1的位,还有可能放皇后的位置 { int put = can_put & -can_put; // 这样运算,put只有can_put的最低位为1的位置为1,其他位置均为0。 dfs(pos|put, (left|put)<<1, (right|put)>>1); can_put ^= put; // 一个二进制位与1异或把该位取反,与0异或该位不变,故该语句把此次放置皇后的位置在can_put中置0(因为这个位置放置皇后的情况已经在此次循环中计算完了)。 } } /* 一个n×n的棋盘,在棋盘上摆n个皇后,求满足任意两个皇后不能在同一行、同一列或同一斜线上的方案数。 n:上述n 返回值:方案数 */ int getAnswer(int n) { ans = 0; allOne = (1 << n) - 1; // 得到一个低n位均为1,其他位为0的二进制。 dfs(0, 0, 0); // 开始棋盘上没有棋子,故pos、left、right的各位均为0。 return ans; } int main() { int n; cin >> n; cout << getAnswer(n) << endl; return 0; }