题目链接。
解析
后边的 dp 方式似乎和已有题解有一些差异。
首先考虑从第 $1$ 张纸片开始一张张放,计算出每张纸片有多少种放法,这分为三种情况:
- 最后局面该纸片占两格,显然只有 $1$ 种放法。
- 最后局面该纸片占一格,那么可能的放法有 $4$ 种,去除其中会覆盖最终局面的放法即可。
- 最后局面不存在该纸片,那么可能的放法为所有未被覆盖的位置的放法之和,这个可以在过程中维护。具体地,初始放法数为 $(n-1)\times(m-1)+n-1+m-1$,然后每次放纸片之后减去那些不能放的放法即可。
容易发现,所有在最终局面出现过的纸片都是必须要选择的,假设这些纸片数为 $k$,那我们还需要选择的纸片数为 $[l-k,r-k]$。对于一种选择方式,其对答案的贡献就是所有选中的纸片的放法数之积。
我们又发现, 所有最终局面不存在的纸片是独立的,即某一张纸片是否选择并不会影响其他纸片的方案数。那么问题转化为求在剩余纸片中分别选 $l-k,l-k+1,\dots,r-k-1,r-k$ 张纸片,并将每一种情况的乘积求和。可以考虑转化为一次求出选 $1,2,3,\dots,r-k$ 张纸片的贡献。
我们考虑选 $1$ 张纸片的贡献就是剩余纸片的和,而选 $2$ 张纸片时,选中第 $i$ 张纸片的贡献就是 $i$ 的方案数乘以上一层的 $i+1$ 的后缀和,然后同理层层处理即可。
举个例子,加入剩余的纸片对应的方案数为 $[7,5,4,2,2]$,求其后缀和为 $[20,13,8,4,2]$,那么选一张纸片的贡献就是 $20$,然后选两张纸片时,先计算出强制选择第 $i$ 张纸片,令其与 $i+1$ 及之后的纸片结合的贡献,即 $[91,40,16,4,0]$,再次求其后缀和为 $[151,60,20,4,0]$,即选两张纸片的贡献为 $151$。
可能说的还不是很明白,可以参考下面的代码进行理解。
代码
#include<bits/stdc++.h>
#define pi pair<int,int>
#define fi first
#define se second
#define mk make_pair
#define int long long
#define getchar()(p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
using namespace std;
char buf[1<<21],*p1=buf,*p2=buf;
template <typename T>
inline void read(T& r) {
r=0;bool w=0; char ch=getchar();
while(ch<'0'||ch>'9') w=ch=='-'?1:0,ch=getchar();
while(ch>='0'&&ch<='9') r=r*10+(ch^48), ch=getchar();
r=w?-r:r;
}
const int N=1005;
const int mod=1e9+7;
int T,n,m,k,l,r,a[N][N],ans[N];
int cnt,tot,px[4]={0,0,1,-1},py[4]={1,-1,0,0};
bool vis[N][N];
pi pos[N];
inline pi check(int x,int y){//找旁边有没有相同的编号
for(int i=0,xx,yy;i<4;i++){
xx=x+px[i],yy=y+py[i];
if(xx<1||yy<1||xx>n||yy>m) continue;
if(a[xx][yy]==a[x][y]) return mk(xx,yy);
}
return mk(0,0);
}
inline void del(int x,int y){//更新还有多少位置可以放
for(int i=0,xx,yy;i<4;i++){
xx=x+px[i],yy=y+py[i];
if(xx<1||yy<1||xx>n||yy>m) continue;
if(!vis[xx][yy]) cnt--;
}
}
int b[N],c[N],d[N];
signed main(){
read(T);
while(T--){
read(n);read(m);read(k);read(l);read(r);
cnt=2*(n-1)*(m-1)+n-1+m-1;tot=0;
for(int i=1;i<=k;i++) pos[i]=mk(0,0),ans[i]=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
vis[i][j]=0,read(a[i][j]);
if(!pos[a[i][j]].fi) pos[a[i][j]]=mk(i,j);
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(a[i][j]==0){//没有被覆盖的点先除去
tot++;del(i,j);
vis[i][j]=1;
}
}
}
for(int i=1;i<=k&&tot!=n*m;i++){
if(!pos[i].fi) ans[i]=cnt;//没有出现的
else{
int x=pos[i].fi,y=pos[i].se;
pi t=check(x,y);
if(t.fi){//出现了两个
int xx=t.fi,yy=t.se;
del(x,y);vis[x][y]=1;
del(xx,yy);vis[xx][yy]=1;
ans[i]=1;tot+=2;
}
else{//出现了一个
for(int j=0,xx,yy;j<4;j++){
xx=x+px[j],yy=y+py[j];
if(xx<1||yy<1||xx>n||yy>m) continue;
if(!vis[xx][yy]) ans[i]++;
}
del(x,y);tot++;
vis[x][y]=1;
}
}
}
int ANS=1,sp=0,Ans=0;
for(int i=1;i<=k;i++){
if(pos[i].fi) ANS=ANS*ans[i]%mod,l--,r--;//一定要选中的
else b[++sp]=ans[i];
}
if(l<=0) Ans=ANS;
c[sp+1]=0;//此处开始为 dp,准确说是求一次答案,算一次前缀和
for(int i=1;i<=r;i++){
if(i!=1){
for(int j=1;j<=sp;j++){
c[j]=b[j]*c[j+1];
}
}
else{
for(int j=1;j<=sp;j++) c[j]=b[j];
}
for(int j=sp-1;j>=1;j--) c[j]=(c[j]+c[j+1])%mod;
if(i>=l) Ans=(Ans+c[1]*ANS%mod)%mod;//更新答案时需要呈上必须选中的贡献
}
printf("%lld\n",Ans);
}
return 0;
}