N 皇后问题 :
在 N * N 的棋盘上,放置 N 个皇后,要求每一行,每一列,每一对角线上均只能放置一个皇后,求可能的方案及方案数。
对于 N = 8,采用回溯法很容易求解。
今天早上用了大约 10 分钟就搞定了,代码如下:
// by rappizit @ 2007-09-10
#include < cstdio >
#include < cmath >
using namespace std;
const int N = 20 ; // 最多 N 个皇后
int col [N]; // col [row] 表示第 row 行的皇后的列位置
int cnt = 0 ; // 解法数目
int n = 0 ;
bool judge ( int row)
{
// judge whether the position (row, col [row]) is OK
for (int i = 0; i < row; i ++)
{
if (col [i] == col [row] || abs (col [row] - col [i]) == row - i)
{
return false;
}
}
return true;
}
void f ( int row)
{
if (row == n)
{
// found a solution
cnt ++;
/*
// print the solution
for (int i = 0; i < n; i ++)
{
for (int j = 0; j < col [i]; j ++)
{
printf ("0 ");
}
printf ("1 ");
for (int j = col [i] + 1; j < n; j ++)
{
printf ("0 ");
}
printf (" ");
}
printf (" ");
*/
}
for (int i = 0; i < n; i ++)
{
col [row] = i;
if (judge (row))
{
f (row + 1);
}
}
}
int main ()
{
scanf ("%d", &n); // 输入皇后数目,不超过 20
f (0);
printf ("total : %d ", cnt);
return 0;
}
当皇后数目大于 13 时, 程序运行要很久。
(大一的时候刚学递归,于一同学就叫我做 8 皇后问题,瞎弄了两天终于搞定。当时还不知道有回溯这种东东,硬是凭着常识做出来了,而代码很难看。其实回溯也是种朴素的算法。)
以下有个使用位运算的程序,效率较高。为了简单化,只是实现解法计数的功能,没有输出各个解法。
// 对于 n < 16 时很快!
#include < iostream >
using namespace std;
unsigned int upperlim;
int cnt;
void r (unsigned row, unsigned ld, unsigned rd)
{
// row, ld, rd 的二进制数字的位 1 分别表示当前行因同列,同正对角线,同负对角线而产生的禁忌位置
if (row == upperlim)
{
cnt ++;
return;
}
unsigned pos = upperlim & ~(row | ld | rd); // pos 的二进制数字的位 1 表示当前行的可放位置
while (pos)
{
unsigned p = pos & (-pos); // 取 pos 的二进制数字的最低位的 1
pos -= p;
r (row + p, (ld + p) << 1, (rd + p) >> 1);
}
}
int main ()
{
int n;
cin >> n; // n < 32, 其实 n = 16 时已经耗时很久了
upperlim = (1U << n) - 1; // upperlim 的二进制数字最低 n 位为 1 ,其余位为 0
cnt = 0;
r (0, 0, 0);
cout << cnt << endl;
return 0;
}
这是在 http://www.matrix67.com/blog/ 看到的,原来是 Pascal 写的,自己改成 C++ 的了。
和普通算法一样,这是一个递归过程,程序一行一行地寻找可以放皇后的地方。过程带三个参数,row、ld和rd,分别表示在纵列和两个对角线方向的限制条件下这一行的哪些地方不能放。我们以6x6的棋盘为例,看看程序是怎么工作的。假设现在已经递归到第四层,前三层放的子已经标在左图上了。红色、蓝色和绿色的线分别表示三个方向上有冲突的位置,位于该行上的冲突位置就用row、ld和rd中的1来表示。把它们三个并起来,得到该行所有的禁位,取反后就得到所有可以放的位置(用pos来表示)。前面说过-a相当于not a + 1,这里的代码第6行就相当于pos and (not pos + 1),其结果是取出最右边的那个1。这样,p就表示该行的某个可以放子的位置,把它从pos中移除并递归调用test过程。注意递归调用时三个参数的变化,每个参数都加上了一个禁位,但两个对角线方向的禁位对下一行的影响需要平移一位。最后,如果递归到某个时候发现row=111111了,说明六个皇后全放进去了,此时程序从第1行跳到第11行,找到的解的个数加一。
=============================注:红色内容为引用原文内容==========================
另外,如果只是求解一种放置皇后的方法的话,利用启发信息可以提高搜索的效率,不过对于皇后数目超过 39 的时候,还是 SB 了。
// by rappizit @ 2007-09-10
#include < cstdio >
#include < cmath >
#include < algorithm >
using namespace std;
const int N = 40 ; // 最多 N 个皇后
int col [N];
int cnt = 0 ;
int n = 0 ;
bool judge ( int row)
{
// judge whether the position (row, col [row]) is OK
for (int i = 0; i < row; i ++)
{
if (col [i] == col [row] || abs (col [row] - col [i]) == row - i)
{
return false;
}
}
return true;
}
void f ( int row)
{
if (row == n)
{
// found a solution
cnt ++;
// print the solution
for (int i = 0; i < n; i ++)
{
for (int j = 0; j < col [i]; j ++)
{
printf ("0 ");
}
printf ("1 ");
for (int j = col [i] + 1; j < n; j ++)
{
printf ("0 ");
}
printf (" ");
}
printf (" ");
}
pair <int, int> diagonal [N];
for (int i = 0; i < n; i ++)
{
// 按照该位置在剩下的棋盘里的两条对角线较长的那条的长度作为启发函数的值
diagonal [i] = pair <int, int> (max (min (i, n - 1 - row), min (n - 1 - i, n - 1 - row)), i);
}
sort (diagonal, diagonal + n);
for (int i = 0; i < n && !cnt; i ++)
{
col [row] = diagonal [i].second; // 带启发性
//col [row] = i; // 盲目
if (judge (row))
{
f (row + 1);
}
}
}
int main ()
{
scanf ("%d", &n); // 输入皇后数目,不超过 40
f (0);
return 0;
}
/*
只搜索一个解的情况下,带启发性的回溯效率提高很多,但是到了 40 皇后的时候还是 SB 了。
*/
其中, 按照该位置在剩下的棋盘里的两条对角线较长的那条的长度作为启发函数的值,因为该值越小,那么那个位置在 剩下的棋盘里 影响的行数就较小,故有希望更容易找到解。