国王(状态压缩)
我发现我的位运算是⑩
所以来梳理一些简单题唤醒大脑
国王
在 \(n*n\) 的棋盘上放 \(k\) 个国王,国王可攻击相邻的 \(8\) 个格子,求使它们无法互相攻击的方案总数
\((n<=10,k<=n*n)\)
从数据范围我们得以知道这是一道状压,我们需要枚举的是棋盘的放置方案,所以 \(f\) 数组其中一维就是状态数以进行枚举,有多行所以我们以行数作为一维进行转移,再以棋子数为一维以统计
得到 \(f_{i,j,k}\) 表示第 \(i\) 行采取第 \(j\) 种方案放了 \(k\) 个棋子的方案数
我们能得到动态转移方程式
for(int i=1;i<=n;i++){
for(int j=1;j<=cnt;j++){//cnt 表示状态数
//枚举当行
for(int p=1;p<=cnt;p++){
//枚举上一行
for(int x=0;x+num[j]<=x;x++){//num 是这种状态的棋子数
f[i][j][x+num[j]]+=f[i-1][p][x];
}
}
}
}
但是还有不合法的情况,所以我们不能这样简单的枚举
那么怎么判断是否合法呢,唯一的限制条件就是两棋子不能相邻
对于同一列,相邻意味着两个 1 挨在一起,如何高效的判断变成了关键,如果要使状态合法的话,两个 1 之间至少相隔 1 个空位,这给予我们启示:
将原状态向右移一位与原状态进行 & 运算 我们发现,若状态合法,那么结果会为 0 ,反之亦然,我们成功简单高效的判断了同一行状态的合法
至于判断竖直上就更简单了,只需要看 1 的上面还有没有 1 就行,直接进行 & 操作,至于斜对角左移右移解决
代码就可以写了,这边用到一个状压很常用的剪枝,提前判断状态筛选,我们发现,在后面进行统计的时候,要是直接枚举所有情况并且进行判断,内层就有 \(2^n*2^n\) 的天文数字,显然会超时,所以我们提前判断水平是否合法并记录到数组中,大大减少了代码复杂度和时间消耗
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=11;
int f[N][1<<N][N*N];
int n,k;
int g[1<<N];
int num[1<<N];
int get(int x){
int cnt=0;
while(x){
if(x&1) cnt++;
x>>=1;
}
return cnt;
}
int cnt;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin>>n>>k;
for(int i=0;i<(1<<n);i++){
if(i&(i<<1)||i&(i>>1)) continue;//初步筛选状态,横向判断是否相邻
// 将合法的状态计入
g[++cnt]=i;
num[cnt]=get(i);//记录当前状态的棋子个数
}
f[0][1][0]=1;
for(int i=1;i<=n;i++){
for(int k1=1;k1<=cnt;k1++){//枚举当行状态
for(int k2=1;k2<=cnt;k2++){//枚举上一行状态
if(g[k1]&g[k2]||(g[k1]>>1)&g[k2]||(g[k1]<<1)&g[k2]) continue;
//筛出不合法状态
for(int k3=0;k3+num[k1]<=k;k3++){
f[i][k1][k3+num[k1]]+=f[i-1][k2][k3];
}
}
}
}
int ans=0;
for(int i=1;i<=cnt;i++){
ans+=f[n][i][k];
}
cout<<ans;
return 0;
}

浙公网安备 33010602011771号