题意,n*m的矩阵,每行选择一个数,每列最多选择所选总数的二分之一的方案数
对于矩阵上的a[i][j]表示i行j列这个位置上有多少个数
也就是说假设有一个合法的方案,第i行选择ch[i]列,这种方案对答案的贡献即为所有a[i][ch[i]]的乘积
大于等于所选总数也就是行数的二分之一的最多只有一列,枚举这一列
可以发现知道了最多的是哪一列之后其他列分别有多少个我们并不关心
对于每一行求个和,选最多列直接乘,选其他列乘上和减去最多列
然后问题就从必须暴力枚举所有方案考虑哪一列是最多的
转变为,只与考虑到哪一行了(没有这个没法转移呀),当前这一列选了多少个和总共选了多少个(或者说其他所有列的和)有关的状态
这个状态转移方程记得是其他所有列的和
f[i][j][k]=f[i-1][j][k]+a[i][col] * f[i-1][j-1][k]+(s[i]-a[i][col]) * f[i-1][j][k-1]
那么dp可以轻松解决,统计结果也很方便,只是会tle
非常奇妙的一点是,其实这一列选了多少个和总共选了多少个的具体数目我们也不关心
我们只需要知道一个差值,也许这一点可以通过观察状态转移方程得来?
统计结果只需要j>k的和与jk具体值无关,转移的时候也只存在差值不变,+1,-1三种,而且不重复
那么f[i][j]=f[i-1][j]+a[i][col] * f[i-1][j-1]+(s[i]-a[i][col]) * f[i-1][j+1]
然后就可以过啦,记得取模
#include<bits/stdc++.h>
#define mod 998244353
using namespace std;
int a[105][2005],s[105];
long long f[2][205],g[105];//f的第二维至少是2*n+1的别开太小
inline int read()
{
int x=0;
char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') x=x*10+ch-'0',ch=getchar();
return x;
}
int main()
{
//freopen("meal.in","r",stdin);
//freopen("meal1.out","w",stdout);
int n=read(),m=read();
for(register int i=1;i<=n;i++)
for(register int j=1;j<=m;j++) a[i][j]=read(),s[i]=s[i]+a[i][j],s[i]%=mod;
long long ans=0;
for(register int k=1;k<=m;k++)
{
memset(f,0,sizeof(f));
f[0][n]=1;
int now=1,last=0;
for(register int i=1;i<=n;i++)
{
for(register int j=n-i;j<=n+i;j++)
f[now][j]=(f[last][j]+f[last][j-1]*a[i][k]%mod+f[last][j+1]*(s[i]-a[i][k])%mod)%mod;
swap(now,last);//滚动数组,虽然并没有快很多
}
for(register int j = 1; j<=n; j++)
ans = (ans+f[last][n+j])%mod;
}
g[0]=1;
for(register int i=1;i<=n;i++) g[i]=(g[i-1]+g[i-1]*s[i])%mod;
cout<<(g[n]-ans-1+mod)%mod;//减法需要+mod%mod
return 0;
}