【题号】HDOJ 2553
【题目描述】
N皇后问题
Time Limit: 2000/1000 MS(Java/Others) Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 8502 Accepted Submission(s): 3787
Problem Description
在N*N的方格棋盘放置了N个皇后,使得它们不相互攻击(即任意2个皇后不允许处在同一排,同一列,也不允许处在与棋盘边框成45角的斜线上。
你的任务是,对于给定的N,求出有多少种合法的放置方法。
Input
共有若干行,每行一个正整数N≤10,表示棋盘和皇后的数量;如果N=0,表示结束。
Output
共有若干行,每行一个正整数,表示对应输入行的皇后的不同放置数量。
Sample Input
1
8
5
0
Sample Output
1
92
10
Author
cgf
【类型】
简单回溯 或 位运算
【分析】
方案一:回溯法
每行放一个,只要判断同一列和对角线上有没有皇后就行了,简单的回溯基础题。记得进行预处理,不然会超时的。
方案二:位运算法(太赞了!简直不能表达我的敬仰之情了!!!)
【参考了Matrix67(http://www.matrix67.com/blog/archives/266)】
即便是不预处理也能低空飘过!(就是这么快!哈哈)
又快又短!!!
和普通算法一样,这是一个递归过程,程序一行一行地寻找可以放皇后的地方。过程带三个参数,row、ld和rd,分别表示在纵列和两个对角线方向的限制条件下这一行的哪些地方不能放。
以6*6的棋盘为例:
upperlim = (1 << i) - 1;其中upperlim的值的二进制位数为i位,且这i位全是1,代表一种放满的状态。下面我们可以用来判断是否放满,比如我们的纵列限制row的二进制如果全是1(即row = upperlim),那么也就是说,纵列全是限位,没有位置可以放皇后,这证明皇后已经放满棋盘(只能用纵列限制row进行判断,只有这种情况才算是放满的状态)。
在图中,绿线、蓝线、黄线分别表示纵列和两个对角线方向的禁位,有冲突的禁位我们分别用row、ld、rd中的1表示,将这三个合并起来我们就可以得到所有的禁位。
例如上面的6*6的棋盘,合并三个之后:(row | ld | rd) = (101111)。
对一个有符号的数进行按位非运算(~)后,最高位的变化将导致正负颠倒,并且数的绝对值会差1。也就是说,~a实际上等于-a-1。这种整数储存方式叫做“补码”(这也是为什么代码中pos & (-pos); 相当于 pos & (~pos + 1))。p = pos & (-pos);的结果是取出pos中二进制补码中最右边的那个1(注意:数据在计算机是以二进制补码存储的,位运算也是将补码进行运算)。(这个操作可以举个例子:比如10的补码为(0000 1010),-10的补码(1111 0110),然后我们计算10 & (-10),很明显,这样按位与之后计算的结果为: 0000 0010,它所代表的值是2,同时它也代表10的二进制补码表示(0000 1010)中最右边的那个1,单独取出来放在p中)。
至于递归参数的变化,这个可以从图上就可以很容易的理解。
温馨提示:代码看起来很长是因为格式和注释的问题!
【code 1】
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;
const int Max = 11;
int flag[Max][Max];
int sum;
int n;
int re[Max];
int is_place(int x, int y)
{
int i,j;
//判断同一列有没有皇后(只需要判断列就好了,因为是每行放一个)
for(i=1; i<x; i++)
{
if(flag[i][y])
return 0;
}
//判断对角线上有没有皇后
for(i=1; i<x; i++)
{
for(j=1; j<=n; j++)
{
if(abs(i - x) == abs(j - y))
{
if(flag[i][j] == 1)
{
return 0;
}
}
}
}
return 1;
}
void queen(int k)
{
if(k == n + 1)
{
sum++;
}
else
{
for(int i=1; i<=n; i++)
{
if(is_place(k, i) && !flag[k][i])
{
flag[k][i] = 1;
queen(k+1);
flag[k][i] = 0;
}
}
}
}
int main()
{
int s;
memset(flag, 0, sizeof(flag));
for(int i=1; i<=10; i++)
{
sum = 0;
n = i;
queen(1);
re[i] = sum;
}
while(scanf("%d",&s),s)
{
cout<<re[s]<<endl;
}
return 0;
}
【code 2】
#include <iostream>
#include <cstdio>
using namespace std;
int sum;
int upperlim;
int n;
int ans[15];
void queen(int row, int ld, int rd)
{
int pos,p;
if(row != upperlim)
{
pos = upperlim & (~(row | ld | rd));
/*
(~(row | ld | rd))可以得到所有可放的位置(这个时候可放的位置用1表示),为什么还要和upperlim
按位与呢?因为取反的时候把(row | ld | rd)的符号位变成1了,而我们仍然要保持符号位为0,就与
upperlim按位与即可(upperlim的符号位为0),这样我们得到的pos就是所有可以放皇后的位置,注意这里
二进制中的1表示可以放皇后的位置。
*/
while(pos != 0) //如果全为0,那就是没有可放的位置
{
p = pos & (-pos); /*相当于 pos & (~pos + 1)[补码表示],其结果是取出最右边的那个1(即单独取出可放棋子的位置),
并且在该1位置我们将放置一颗棋子 */
pos = pos - p; /*从所有可放的位置取出我们选定的放皇后的点,以便于下次回溯的时候选取另外的点放置皇后
这里同样是个补码运算,可以自己算一下*/
queen(row+p, (ld+p)<<1, (rd+p)>>1); //注意递归调用时三个参数的变化,每个参数都加上了一个禁位,
//但两个对角线方向的禁位对下一行的影响需要平移一位,这个根据几何位置也很好理解。
}
}
else
sum++;
}
int main()
{
for(int i=1; i<=10; i++)
{
upperlim = (1 << i) - 1;
/*这样就可以得到一个二进制后i位为1(其余均为0,包括符号位为0)的数upperlim,代表皇后放满状态。
棋盘的位置上,1代表已经存在皇后,0代表不存在皇后。
关于这种运算,我们可以举个例子:假如i=3;
(为了方便,我们这里用8位二进制来表示),(1 << i)的补码为:0000 1000,
而补码的任何运算都可以变成加法运算 (i << i) - 1 = (i << i) + (-1) ,
-1的补码为:1111 1111,(i << i) + (-1) = 0000 1000 + 1111 1111 = 0000 0111 = 7(10进制)
*/
sum = 0;
queen(0,0,0);
ans[i] = sum;
}
while(scanf("%d",&n),n)
{
cout<<ans[n]<<endl;
}
return 0;
}