P7716 「EZEC-10」Covering 题解

发布于 2021-10-28  71 次阅读


题目链接

文章目录

解析

后边的 dp 方式似乎和已有题解有一些差异。

首先考虑从第 $1$ 张纸片开始一张张放,计算出每张纸片有多少种放法,这分为三种情况:

  1. 最后局面该纸片占两格,显然只有 $1$ 种放法。
  2. 最后局面该纸片占一格,那么可能的放法有 $4$ 种,去除其中会覆盖最终局面的放法即可。
  3. 最后局面不存在该纸片,那么可能的放法为所有未被覆盖的位置的放法之和,这个可以在过程中维护。具体地,初始放法数为 $(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;
}

月流华 岁遗沙 万古吴钩出玉匣