\(N\) 皇后问题
Description
在 \(N \times N\) 的方格棋盘放置 \(N\) 个皇后,使得它们不相互攻击(即任意 \(2\) 个皇后不允许处在同一排,同一列,也不允许处在与棋盘边框成 \(45^\circ\) 角的斜线上)。
你的任务是,对于给定的 \(N\),求出有多少种合法的放置方法。
Solution
- 一些定义
- \(row\) 皇后对其所在列的攻击,
0
表示所在列是安全的,1
反之。 - \(left\) 皇后对其自左上角向右下的主对角线上的攻击,
0
表示所在主对角线是安全的,1
反之。 - \(right\) 皇后对其自右上角向左下的副对角线上的攻击,
0
表示所在副对角线是安全的,1
反之。 - \(free\) 综合上三种情况,所有列的情况,
1
表示所在列是安全的,0
反之。 - \(first\) 从前往后,第一个安全的列,只有一个
1
表示安全,其余都是0
。
注意:\(row,left,right\) 都是 \(0\) 表示当前列安全(空闲)且都是递归的参数,而 \(free,first\) 都是 \(1\) 表示当前列能够放皇后,是在递归中的临时变量。
- 状压最大状态
左移的开始开始只有一个 \(1\),设 \(n=8\),为 100000000
,然后减 \(1\),借位,为 11111111
。\((1<<n)-1\) 经常表示最大状态。
- 求 \(¥\)free$
所有列危险即 \(row\) 都是 1
,所以当 \(row=(1<<n)-1\),回溯。
如果 \(row,left,right\) 中任意个对第 \(i\) 列攻击,那么第 \(i\) 列危险即为
1
。因为要做到一个为真结果为真,所以取反\(\sim row| left|right\),第 \(i\) 个安全即为
1
。但是,取反得到的答案是负数,所以 \(free = ((1<<n)-1)\&(\sim row|left|right)\)。
注意:取反一定要小心使用。对无符号数 \(n\) 取反得到的是 \(-(n+1)\),可以通过与上 \((1<<n)-1\)转成正数。
- 求 \(last\)
如果\(n\& \sim n\),结果为 \(0\),那么\(n\& \sim n+1\)呢?
假设二进制数从前往后第一个
1
在 \(pos\),那么 \(pos\) 在取反之前为空或0
,取反之后为0
(每次都把从前往后第一个安全的列放皇后,所以取到 \(pos\) 时,\(pos\) 之后的位置已经全为0
)。取反后第一个
0
在 \(pos\),之前为空或1
,之后全为1
。对取反后的二进制加 \(1\),由于pos
之后全为1
,所以加 \(1\) 的时候回往前 进位,进位后 \(pos\) 之后的位置全为0
,\(pos\) 列已经接受到了后面传来的进位成为1
。- 两个二进制数与后,\(pos\)前和 \(pos\) 后全部
0 0
为0
。只剩下 \(pos\) 一个位置1 1
为1
,现在二进制数就是需要的 \(first=free \& (\sim free + 1)\) 。 在将 \(last\) 处理后,如何得到下一个 \(last\) 呢?因为 \(last\) 是根据 \(free\) 得到的,所以需要进行更新。或运算是不可行的,因为 \(1|0=0\),而原来所有在 \(free\) 能放的位置在或运算后就不能放了。可以进行 \(free\&= \sim last\) ,这样就成功更新了。
- 下一层递归
- \(row\) 如何进入下一层递归?当前已经将 \(pos\) 列放上了皇后, 那么如何将 \(row\) 的 \(pos\) 位改为
1
呢?因为 \(row\) 与 \(last\) 中0
和1
表示的意义正好相反,所以为 \(row|last\) 。 - \(left\) 如何进入下一层递归?同样用更新 \(row\) 的方法更新 \(left\)。但是因为对角线是呈\(45^\circ\) 角倾斜的,这一列 \(pos\) 位是 \(0\),到了下一行,
pos+1
一定是在 \(pos+1\) 位的(如果存在)。所以为 \((left|last)>>1\)。 - \(right\) 进入下一层递归的方法可参照 \(left\) 进入下一层递归的方法,把右移改成左移即可。为\((right|last)<<1\)。
Code
#include<bits/stdc++.h>
using namespace std;
int n,ans;
int INF;
void dfs(int row,int left,int right)
{
if(row==INF)
{
ans++;
return ;
}
int free=INF&~(row|left|right);
while(free)
{
int last=free&((~free)+1);
free&=~last;
dfs(row|last,(left|last)>>1,(right|last)<<1);
}
}
int main()
{
while(~scanf("%d",&n)&&n)
{
INF=(1<<n)-1; ans=0;
dfs(0,0,0);
printf("%d\n",ans);
}
return 0;
}
还是 \(N\) 皇后
Description
有一个 \(N \times N\) 的方格棋盘,有的位置是 *
,表示能放置皇后;有的位置是.
,表示不能放置皇后。需要放置 \(N\) 个皇后,使得它们不相互攻击(即任意 \(2\) 个皇后不允许处在同一排,同一列,也不允许处在与棋盘边框成 \(45^\circ\) 角的斜线上)。
你的任务是,对于给定的 \(N\),求出有多少种合法的放置方法。数据保证有解。
Solution
还是 \(N\) 皇后与 \(N\) 皇后问题的最大不同在于还是 \(N\) 皇后会限制某几个方格不能放置皇后。
在输入的棋盘中,用 \(a_i\) 表示第 \(i\) 行的每一列是否可以放置皇后。如何在输入的时候预处理呢?在 \((i,j)\) 读入到 .
时,需要将 \(a_i\) 的第 \(j\) 位变成 \(1\),为 \(a_i|=(1<<(j-1))\)。在搜索中,新增一个参数 \(lev\) 表示当前搜索至第 \(lev\) 行,然后将int free=INF&~(row|left|right);
修改成int free=INF&~(row|left|right|a[lev]);
即可。
Code
#include<bits/stdc++.h>
using namespace std;
int n,ans,INF;
int a[101];
char s[101];
void dfs(int row,int left,int right,int lev)
{
if(row==INF)
{
ans++;
return ;
}
int free=INF&~(row|left|right|a[lev]);
while(free)
{
int last=free&((~free)+1);
free&=~last;
dfs(row|last,(left|last)>>1,(right|last)<<1,lev+1);
}
}
int main()
{
scanf("%d",&n); INF=(1<<n)-1;
for(int i=1;i<=n;i++)
{
scanf("%s",s);
for(int j=0;j<n;j++)
if(s[j]=='.') a[i]|=(1<<j);
}
dfs(0,0,0,1);
printf("%d\n",ans);
return 0;
}