国王(状态压缩)

我发现我的位运算是

所以来梳理一些简单题唤醒大脑

国王

\(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;
} 
posted @ 2025-07-10 15:58  Zom_j  阅读(13)  评论(0)    收藏  举报