P3977 [TJOI2015]棋盘 题解

题目传送门
一道结合了状压和矩阵快速幂的好题。

题目有一处bug就是这里的 \(p\) 应该是不大于m的……

废话不多说,首先观察到 \(m\) 很小,每一行最多就有 \(64\) 种状态,所有首先考虑状压每一种摆放方案。这里可以搜索出来,具体可以参考P1896 互不侵犯这个题。同样,我们可以预先处理出每一种放置方案所对应的攻击范围。

然后,就考虑转移。对于每一行的每一种方案,都由上一行不会被下一行攻击到,并且不会攻击到下一行的方案转移而来。那么,对于第k行,有

\[dp_{k, i} = \sum_{j = 1}^{tot}dp_{k-1,j} \]

其中 \(tot\) 为方案总数,且只有第 \(k-1\) 行的第 \(j\) 种方案合法才可以转移。

我们发现对于每一行,能转移的上一行方案其实是相同的,同时这个转移方程非常符合矩阵乘法的定义,所有考虑到用矩阵快速幂优化。

注意最后统计答案时,可以看作第 \(n+1\) 行为空行,也就是说,我们只需要统计可以转移到这个空行的方案总数就行了,因为空行不会受限制。

代码:

#include<bits/stdc++.h>
#define usi unsigned int
using namespace std;
const int N = 1e6+100;

inline int read(){
	int x = 0; char ch = getchar();
	while(ch<'0'||ch>'9'){
		ch = getchar();
	}
	while(ch>='0'&&ch<='9'){
		x = x*10+ch-48; ch = getchar();
	}
	return x;
}

int atk[4], po[4][80], tot;//攻击范围,每一种摆放方案及攻击范围,方案总数。 
int n, m;
int p, K;

struct mat{
	usi f[80][80];
	int lth;
	mat(){
		memset(f, 0, sizeof(f));
		lth = tot;
	}
	usi * operator [](int x){return f[x];}
	mat operator*(mat B){
		mat t, BT;
		for(int i = 0; i<lth; i++){
			for(int j = 0; j<lth; j++){
				BT[i][j] = B[j][i];
			}
		}
		for(int i = 0; i<lth; i++){
			for(int j = 0; j<lth; j++){
				for(int l = 0; l<lth; l++){
					t[i][j]+=f[i][l]*BT[j][l];
				}
			}
		}
		return t;
	}
	void build(){
		for(int i = 0; i<lth; i++){
			f[i][i] = 1;
		}
	}
	void print(){
		for(int i = 0; i<lth; i++, puts("")){
			for(int j = 0; j<lth; j++){
				printf("%d ", f[i][j]);
			}
		}
	}
};


mat a, b;
void dfs(int x, int pos, int y){
	if(pos>=m){
		po[1][++tot] = y;		
		return;
	}
	if(pos<K){
		if((((1<<pos)&x)==0)&&((((atk[1]>>(K-pos))&y)==0)))dfs(x+(atk[1]>>(K-pos)), pos+1, y+(1<<pos));
		dfs(x, pos+1, y);
	}
	else{
		if((((1<<pos)&x)==0)&&((((atk[1]<<(pos-K))&y)==0))) dfs(x+(atk[1]<<(pos-K)), pos+1, y+(1<<pos));
		dfs(x, pos+1, y);
	}
}//这样的搜索方式可以保证最后一种方案是空行。
 
inline mat fpow(mat x, int y){
	mat tmp;
	tmp.build();
	while(y){
		if(y&1){
			tmp = tmp*x;
		}
		x = x*x;
		y>>=1;
	}
	return tmp;
}

int main(){
	n = read(), m = read();
	p = read(), K = read();
	for(int i = 0; i<3; i++){
		for(int j = 0; j<p; j++){
			int t = read();
			atk[i]+=(1<<j)*t;
		}
	}
	
	atk[1]-=(1<<K);//注意我们钦定自己不会攻击自己。 
	int up = (1<<m);
	dfs(0, 0, 0);
	for(int i = 1; i<=tot; i++){
		for(int j = 0; j<m; j++){
			if((po[1][i]>>j)&1){
				if(j<K){
					po[0][i]|=(atk[0]>>(K-j));
					po[2][i]|=(atk[2]>>(K-j));
				}
				else{
					po[0][i]|=(atk[0]<<(j-K));
					po[2][i]|=(atk[2]<<(j-K));
				}
			}
		}
	}//预处理各个情况的攻击范围,注意位运算要倒腾清。 
	
	for(int i = 1; i<=tot; i++){
		for(int j = 1; j<=tot; j++){
			if(((po[1][i]&po[2][j])==0)&&((po[0][i]&po[1][j]))==0){
				a[i-1][j-1] = 1;//将合法的转移方案加入矩阵 
			}
		}
	}
	
	a.lth = tot;
	a = fpow(a, n);
	b[tot-1][tot-1] = 1;
	b.lth = tot;
	b = b*a;
	usi ans = 0;
	
	for(int i = 0; i<tot; i++){
		ans+=b[tot-1][i];
	}//统计答案 

	printf("%u\n", ans);
	return 0;
}
posted @ 2023-06-07 10:41  霜木_Atomic  阅读(20)  评论(0编辑  收藏  举报