不得不佩服这道题的玄妙
首先我们可以发现一个显然的性质:任意一行或任意一列至多只有两个炮
然后就有一种50分的做法:把每一行的情况三进制状压,然后状压DP即可
转移从上面找出所有有0个炮,1个炮,2个炮的列然后枚举新加入的炮的个数
然后我们瞎搞一下可以发现,其实方案总数与炮摆放的位置无关
即我们根本不需要记录炮在第几行第几列,直接记录一下上一行放了0个炮,1个炮,2个炮的列数各有几个然后就可以直接DP了
同样可以优化,因为一行的位置都是m,因此只要知道1个炮的列数和2个炮的列数就可以用m减去他们得到0个炮的列数
因此我们设f[i][j][k]表示当前第i行,有j列有1个炮,有k列有2个炮的方案总数
则可以由f[i][j][k]推得f[i+1]的许多状态
这里的转移有:
-
f[i+1][j][k]+=f[i][j][k] (一个炮也不放)
-
f[i+1][j+1][k]+=f[i][j][k]*(m-i-j)(m-j-k>=1) (在没有炮的列上放一个炮)
-
f[i+1][j-1][k+1]+=f[i][j][k]*j(j>=1) (在只有一个炮的位置上放一个炮)
-
f[i+1][j+2][k]+=f[i][j][k]*(m-j-k)*(m-j-k-1)/2(m-j-k>=2) (在没有炮的位置上放两个炮,这里的方案数要等差数列求和(组合数也可以))
-
f[i+1][j-2][k+2]+=f[i][j][k]*j*(j-1)/2(j>=2)(在有一个炮的位置上放两个炮)
-
f[i+1][j][k+1]+=f[i][j][k]*(m-j-k)*j(m-j-k>=1&&j>=1)(在有一个炮和有没有炮的位置上各放一个炮)
然后就很舒服了,最后求一下所有f[n+1][j][k]的和即可
边界条件:f[1][0][0]=1;
这里由于DP方程只需要由f[i]推得f[i+1],因此可以滚动优化然而这个数据范围还是不需要了,但我仍然滚存了
CODE
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long LL;
const LL N=105,mod=9999973;
LL f[2][N][N],ans;
int n,m;
inline LL C(LL x)
{
return (x*(x-1)/2)%mod;
}
inline void inc(LL &x,LL y)
{
if ((x+=y)>=mod) x-=mod;
}
int main()
{
register int i,j,k;
scanf("%d%d",&n,&m);
f[1][0][0]=1;
for (i=1;i<=n;++i)
{
int now=i&1,nxt=now^1;
memset(f[nxt],0,sizeof(f[nxt]));
for (j=0;j<=m;++j)
for (k=0;j+k<=m;++k)
if (f[now][j][k])
{
inc(f[nxt][j][k],f[now][j][k]);
if (m-j-k>=1) inc(f[nxt][j+1][k],(f[now][j][k]*(m-j-k))%mod);
if (j>=1) inc(f[nxt][j-1][k+1],(f[now][j][k]*j)%mod);
if (m-j-k>=2) inc(f[nxt][j+2][k],(f[now][j][k]*C(m-j-k))%mod);
if (j>=2) inc(f[nxt][j-2][k+2],(f[now][j][k]*C(j))%mod);
if (m-j-k>=1&&j>=1) inc(f[nxt][j][k+1],(f[now][j][k]*(m-j-k)*j)%mod);
}
}
for (j=0;j<=m;++j)
for (k=0;k+j<=m;++k)
inc(ans,f[(n+1)&1][j][k]);
printf("%lld",ans);
return 0;
}