题目描述
在 $ N×N $ 的棋盘里面放 (K) 个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共 (8) 个格子。
输入格式
只有一行,包含两个数 (N,K) (( 1 <=N <=9, 0 <= K <= N * N))
输出格式
所得的方案数
输入输出样例
输入 #1
3 2
输出 #1
16
思路
其实一开始我以为这是一道搜索题,因为长得的确很像。
但这是一道状压dp。然而我也不是很清楚什么是状压dp。应该就是把一个状态压缩成一个二进制数,从而减少空间复杂度。那么这个题如何将状态转化为二进制数呢?其实并不难,甚至很显而易见,我们可以用一个二进制数来表示一行内国王的摆放,比如(1010101)这个数,就表示第(1,3,5,7)列有国王,反之(2,4,6)列就没有,设计好状态,那么怎么转移状态呢?
首先我们要判断本身这个数能不能单独摆放在一行里,可以预处理解决。其次我们要判断下一行的摆放会不会和当前这一行的摆放矛盾。其实代码实现很简单,我们只需要将下一行的二进制数分别左移和右移一位再相比较就可以了。因为一个国王占领了周围八个格子,所以左边一列和右边一列都不能有国王,在二进制数上就体现为相邻位上只能有一个(1)。左移一位,右移一位,再加上它本身。然后和当前这一行进行(&)这一位运算,不难想到,如果符合条件的话,那么运算后得出的结果应该都是(0)。转移条件就也解决了。
最后考虑初始化和状态转移方程。可以用一个三维数组f[i][j][k]表示第i行的状态是j(转化为二进制后表示状态),而且已经放了k个国王。那么状态转移方程就是f[i][j][k+tot(j)]+=f[i-1][s][k],其中tot函数用来算出二进制下状态中(1)的个数,也就是当前这一行国王的个数,而(s)表示上一行里符合条件的状态,枚举即可。初始化只需要初始第一行即可,只要放的国王小于给出的(k)(好像变量名重复了),那么就将方案数变为(1)。然后就暴切蓝题了。
不开longlong见祖宗
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#include<cstdlib>
#include<ctime>
using namespace std;
int n,k,cnt,b[2000];
long long ans,f[10][2000][100];
int tot(int x){
int sum=0;
while(x){
if(x&1){
sum++;
}
x>>=1;
}
return sum;
}//统计1的个数
int main()
{
scanf("%d%d",&n,&k);
int p=pow(2,n)-1;
for(int i=0;i<=p;i++){
// printf("%d %d
",i>>1,i<<1);
int r=i&(i>>1),t=i&(i<<1);
// printf("%d %d
",r,t);
if((r==0)&&(t==0)){
b[++cnt]=i;
// printf("%d
",i);
}
}//预处理,先将每一行的符合条件的情况预处理出来,这样循环是直接循环满足条件的即可,不用再循环冗余情况后判断
for(int i=1;i<=cnt;i++){
if(tot(b[i])<=k){//不能超过k
f[1][b[i]][tot(b[i])]=1;//第一行不管怎样放都不会有重复情况,所以方案数都是1
}
}
for(int i=2;i<=n;i++){
for(int j=1;j<=cnt;j++){
for(int p=1;p<=cnt;p++){
if(b[j]&b[p]) continue;
if(b[j]&(b[p]<<1)) continue;
if(b[j]&(b[p]>>1)) continue;//判断当前这一行和上一行是否合法
for(int s=1;s<=k;s++){
if(tot(b[j])+s<=k){
f[i][b[j]][tot(b[j])+s]+=f[i-1][b[p]][s];//如果放的国王没有超过个数,那么方案数累加
}
}
}
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=cnt;j++){
ans+=f[i][b[j]][k];//因为不一定是在哪一行哪一个状态放够了k个,那么就累加答案
}
}
printf("%lld
",ans);
return 0;
}