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;
}