P3977 [TJOI2015] 棋盘题解

题目描述

有个 \(n\)\(m\) 列的棋盘,棋盘上可以放许多棋子。每个棋子的攻击范围是 \(3\)\(p\) 列。输入数据用一个 \(3\times p\) 的矩阵给出了棋子攻击范围的模板,棋子被默认为模板中的第 \(2\) 行,第 \(k+1\) 列,则棋子能攻击到的位置是 \(1\),不能攻击到的位置是 \(0\)。输入数据保证第 \(2\) 行第 \(k+1\) 列的位置是 \(1\)。求在棋子互相不能攻击到的前提下,摆放棋子的方案数,答案对 \(2^{32}\) 取余数。

对于 \(100\%\) 的数据,\(1 \leq n \leq 10^{6}\)\(1 \leq m \leq 6\)

题目分析

\(m\) 很小,考虑状压 \(\text{DP}\)

第一步,状压。对于每一行的摆放棋子的情况,我们用二进制压缩,\(0\) 代表改位置不摆放,反之为 \(1\)

第二步,状态总量简化。首先并非 \([0,2^m-1]\) 都是合法行状态,通过枚举,将合法状态存在一个数组 \(S\) 中,这将是之后 \(\text{DP}\) 转移时的可行方案库!

一个小小的问题是,如何利用给定的棋子攻击范围的模板,来快速判断行状态是否合法?显然,行状态的合法性取决于第二行的攻击模板,但是由于攻击模板是以 \(k+1\) 为基准的,所以我们枚举待判定的行中的所有 \(1\),将攻击模板左移 \(\text{or}\) 右移使得 \(k+1\) 与该位置对齐,判断平移后的攻击模板是否与待判定的行有重叠即可(即按位或结果是否非零),这部分的复杂度是 \(O(m)\) 的,因此第二步的总复杂度为 \(O(m2^m)\)

第三步,\(\text{DP}\)。由于棋子攻击范围的模板只有三行,所以转移只需考虑上一行的情况即可。设 \(dp_{i,j}\) 表示第 \(i\) 行摆放棋子的方案为 \(S_j\) 时,前 \(i\) 行构成的子棋盘的所求方案数。则:

\[dp_{i,j}=\sum\limits_{t = 1}^{|S|}{dp_{i-1,t}[check(S_t,S_j)]} \]

其中求和中的括号为艾弗森括号,\(check(S_t,S_j)\) 表示 \(S_t\) 在上一行是否会与 \(S_j\) 冲突。判断方法由第二步如法炮制即可。\(\text{DP}\) 的总复杂度是 \(O(nm(2^m)^2)\)。预处理 \(check\) 的所有情况则可做到 \(O(n4^m)\),由于 \(n\) 很大,这样的做法是很难通过本题的。

第四步,优化。显然这个算法的瓶颈在于 \(n\),但是如何才能把 \(n\) 优化掉呢?在众多 \(\text{DP}\) 优化中,最常见的对 \(n\) 优化的方法当属矩阵加速(快速幂)。因此我们用矩阵的眼光重新梳理转移式:

\[\begin{pmatrix} dp_{i,1} & dp_{i,2} & \dots & dp_{i,|S|} \end{pmatrix} = \begin{pmatrix} dp_{i-1,1} & dp_{i-1,2} & \dots & dp_{i-1,|S|} \end{pmatrix} \begin{pmatrix} check(S_1,S_1) & check(S_1,S_2) & \dots & check(S_1,S_{|S|}) \\ check(S_2,S_1) & check(S_2,S_2) & \dots & check(S_2,S_{|S|}) \\ \dots & \dots & \dots & \dots\\ check(S_{|S|},S_1) & check(S_{|S|},S_2) & \dots & check(S_{|S|},S_{|S|}) \\ \end{pmatrix} \]

这样,我们答案就为:

\[\begin{pmatrix} 1 & 1 & \dots & 1 \end{pmatrix} \begin{pmatrix} check(S_1,S_1) & check(S_1,S_2) & \dots & check(S_1,S_{|S|}) \\ check(S_2,S_1) & check(S_2,S_2) & \dots & check(S_2,S_{|S|}) \\ \dots & \dots & \dots & \dots\\ check(S_{|S|},S_1) & check(S_{|S|},S_2) & \dots & check(S_{|S|},S_{|S|}) \\ \end{pmatrix}^{n-1} \]

配合矩阵快速幂求解,时间复杂度:\(O(8^m\log n)\)

第五步,小细节。对于取模,由于模数为 \(2^{32}\),只需用 \(\text{unsigned}\) \(\text{int}\) 自然溢出即可。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef unsigned int ui;
const int N=1e6+10;
int n,m,p,k,a[4],x,S[74],cnt;
ui Ans;
struct mat {
	ui a[74][74];
	int r,c;
	mat() {
		memset(a,0,sizeof(a));
	}
};
mat operator * (mat x,mat y) {
	mat z;
	z.r=x.r,z.c=y.c;
	for(int i=1;i<=x.r;i++)
		for(int j=1;j<=y.c;j++)
			for(int k=1;k<=x.c;k++)
				z.a[i][j]=z.a[i][j]+x.a[i][k]*y.a[k][j];
	return z;
}
mat ksm(mat x,int y) {
	mat ret=x;
	y--;
	while(y) {
		if(y&1) ret=ret*x;
		x=x*x;
		y>>=1;
	}
	return ret;
}
mat G,sta,ans;
bool check1(int u) {
	for(int i=1;i<=m;i++) {
		if((u>>(i-1))&1) {
			if(i-k<0) {
				if(u&(a[2]>>(k-i))) return false;
			}
			else {
				if(u&(a[2]<<(i-k))) return false;
			}
		}
	} 
	return true;
}
bool check2(int u,int v) {
	for(int i=1;i<=m;i++) {
		if((u>>(i-1))&1) {
			if(i-k<0) {
				if(v&(a[3]>>(k-i))) return false;
			}
			else {
				if(v&(a[3]<<(i-k))) return false;
			}
		}
	} 
	for(int i=1;i<=m;i++) {
		if((v>>(i-1))&1) {
			if(i-k<0) {
				if(u&(a[1]>>(k-i))) return false;
			}
			else {
				if(u&(a[1]<<(i-k))) return false;
			}
		}
	} 
	return true;
}
signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m>>p>>k;
	k++;
	for(int i=1;i<=3;i++) {
		for(int j=1;j<=p;j++) {
			cin>>x;
			if(i==2&&j==k) x=0;
			a[i]|=(x<<(j-1)); 
		}
	} 
	for(int i=0;i<(1<<m);i++) 
		if(check1(i)) S[++cnt]=i;
	sta.r=1,sta.c=cnt;
	G.r=G.c=cnt;
	for(int i=1;i<=cnt;i++) {
		sta.a[1][i]=1;
		for(int j=1;j<=cnt;j++)
			if(check2(S[i],S[j])) G.a[i][j]=1;
	}
	ans=sta*ksm(G,n-1);
	for(int i=1;i<=cnt;i++) Ans+=ans.a[1][i];
	cout<<Ans;
	return 0;
}

posted @ 2025-10-01 22:00  2021hych  阅读(12)  评论(0)    收藏  举报