Link
设(f_{i,j})为(i)行(j)列,强制每行都有黑格子的方案数,那么答案就是(sumlimits_{i=1}^n{nchoose i}f_{i,m})。
考虑(f_{i,j})新增一列转移到(f_{i+k,j+1})有哪些情况:
若(k=0),那么相当于在第(j)列选(0sim2)个端点,贡献系数为(1+i+{ichoose2})。
若(k>0),那么我们考虑求(b_{j+1}-1,c_{j+1}+1)不同的方案。注意这两个格子都是白色的,因此不会在多出来的(k)行,贡献系数为({i+k+2choose k+2})。具体来说是在(i+k+2)行(因为是(b_j-1,c_j+1))中选(k+2)行,把最上/下面一行作为(b_{j+1}-1,c_{j+1}+1),中间的(k)行作为新的(k)行。
注意到第二部分的转移是一个卷积的形式,NTT优化即可。
#include<cctype>
#include<cstdio>
#include<cstring>
#include<algorithm>
const int N=1<<14|1,P=998244353;
int n,m,lim,fac[N],ifac[N],w[N],rev[N],f[N],g[N],t[N];
int read(){int x=0,c=getchar();while(isspace(c))c=getchar();while(isdigit(c))(x*=10)+=c&15,c=getchar();return x;}
void inc(int&a,int b){a+=b-P,a+=a>>31&P;}
void dec(int&a,int b){a-=b,a+=a>>31&P;}
void mul(int&a,int b){a=1ll*a*b%P;}
int pow(int a,int b){int r=1;for(;b;b>>=1,mul(a,a))if(b&1)mul(r,a);return r;}
void init(int n)
{
int half=(lim=1<<(33-__builtin_clz(n)))/2,g=pow(3,(P-1)/lim);
w[half]=fac[0]=ifac[0]=1;
for(int i=1;i<lim;++i) rev[i]=(rev[i>>1]>>1)|(i&1? half:0),mul(fac[i]=fac[i-1],i),ifac[i]=pow(fac[i],P-2);
for(int i=half+1;i<lim;++i) mul(w[i]=w[i-1],g);
for(int i=half-1;i;--i) w[i]=w[i<<1];
}
void NTT(int*a,int f)
{
if(!~f) std::reverse(a+1,a+lim);
for(int i=1;i<lim;++i) if(i<rev[i]) std::swap(a[i],a[rev[i]]);
for(int i=1;i<lim;i<<=1) for(int j=0,d=i<<1;j<lim;j+=d) for(int k=0,x;k<i;++k) x=1ll*a[i+j+k]*w[i+k]%P,dec(a[i+j+k]=a[j+k],x),inc(a[j+k],x);
if(!~f) for(int i=0,x=P-(P-1)/lim;i<lim;++i) mul(a[i],x);
}
int main()
{
int n=read(),m=read(),ans=0;
init(n),memcpy(g+1,ifac+3,4*n),NTT(g,1),f[0]=t[0]=1;
for(int i=1;i<=m;++i)
{
NTT(f,1);
for(int j=0;j<lim;++j) mul(f[j],g[j]);
NTT(f,-1);
for(int j=0;j<=n;++j) t[j]=(1ll*f[j]*fac[j+2]+t[j]*(j*(j+1)/2+1ll))%P,mul(f[j]=t[j],ifac[j]);
memset(f+n+1,0,4*(lim-n-1));
}
for(int i=0;i<=n;++i) inc(ans,1ll*ifac[i]*ifac[n-i]%P*t[i]%P);
mul(ans,fac[n]),printf("%d",ans);
}