[CSP-S2019] Emiya 家今天的饭
Emiya 家今天的饭
题目大意
给出一个 \(n\) 行 \(m\) 列的矩阵,第 \(i\) 行 \(j\) 列的值 \(a_{i,j}\) 表示 \(Emiya\) 会用第 \(i\) 种烹饪方式与第 \(j\) 种主食材做 \(a_{i,j}\) 道菜。
对于一种包含 \(k\) 道菜的搭配方案,有三个要求:
-
至少包含一道菜。
-
每道菜的烹饪方法互不相同。
-
每种主食材被使用的次数不超过 \(\lfloor \frac{k}{2} \rfloor\) 。
求有多少种搭配方案。
分析
前两个限制
三个限制条件,前两个限制条件比较容易满足,第一个不加赘述,第二个限制可以很简单的转化为每一行只能选择一种菜肴,第三个限制条件的满足显然要困难一些。
既然这样,我们先假设没有第三个限制条件,即只考虑前两个限制条件。
设 \(g[i][j]\) 表示前 \(i\) 行选择了 \(j\) 道菜,显然 \(0\leq j\leq i\) 。
转移也很明显是:
即两种情况:
-
第 \(i\) 行选。
-
第 \(i\) 行不选。
对于第 \(i\) 行选,\(sum[i]\) 表示第 \(i\) 行菜肴的总数量,即:
而我们只考虑前两个限制条件的答案此时也是呼之欲出:
从 \(1\) 开始时因为限制条件 \(1\) ,只到 \(n\) 则是由于限制二所以我们最多选择 \(n\) 道菜。
到这里,我们就算做出了前两个限制条件的情况,即我们的 \(ans\) 。
第三个限制
做到这里,我们想要采取的方法也是很明显了,就是想要做一个容斥,从 \(ans\) 里面减去不合法方案。
怎么求不合法方案呢?有一个常见的套路是,我们可以固定一列,令其不合法。
到这里其实我们已经和行没有太大的关系,所以我们考虑枚举列 \(c\) 。
设 \(f[i][j][k]\) 表示前 \(i\) 行 \(c\) 列选择了 \(j\) 个,其他列选择了 \(k\) 个。
转移和我们之前 \(g\) 的转移也是非常的像:
讨论的其实也是两种情况,只不过对于第 \(i\) 行要选,又分了两种:
-
第 \(i\) 行选。
-
选在第 \(c\) 列。
-
选在其他列。
-
-
第 \(i\) 行不选。
求出我们第 \(c\) 列的 \(f\) 数组后,我们要容斥掉哪些情况?
很显然我们只需要容斥掉 \(j>k\) 的情况即可。
则:
将近 \(O(mn^2)\) 的复杂度 ,显然是不可过的,考虑优化。
我们发现我们关心的不是 \(j\) 和 \(k\) 的具体数值大小,而是它们之间的大小关系,后面的两维可以合成一维。
设 \(f[i][j]\) 表示前 \(i\) 行中,第 \(c\) 列比其他列多选 \(j\) 道菜。
我们也能够很轻易的写出状态转移:
其实和三维的状况是一样的,这里就不赘述了。
说说需要注意的地方吧,第二维上说比其他列多选,其实有可能多选“负数”道菜,什么意思呢?有可能少选,于是我们这里要用上一个技巧,将 \(n\) 看做是零,即 \(n+1\) 看做是多做 \(1\) 道菜,那 \(n-1\) 也就是少做 \(1\) 道菜,到这里也就差不多结束了,最后的答案是:
CODE
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e2+10,M=2e3+10,MOD=998244353;
inline int read()
{
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9') { if(ch=='-') w*=-1; ch=getchar(); }
while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
return s*w;
}
int n,m,ans;
int a[N][M];
int sum[N],f[N][2*N],g[N][N];
//状态:f[i][j]表示前i行当前列比其他列多j个
signed main()
{
//freopen("P5664_23 (1).in","r",stdin);
//freopen("meal.out","w",stdout);
n=read(),m=read();
for(register int i=1;i<=n;i++){
for(register int j=1;j<=m;j++){
a[i][j]=read();
sum[i]=(sum[i]+a[i][j])%MOD;
}
}
g[0][0]=1;
for(register int i=1;i<=n;i++){
g[i][0]=1;
for(register int j=1;j<=i;j++)
g[i][j]=(g[i-1][j]+g[i-1][j-1]*sum[i]%MOD)%MOD;
}
for(register int i=1;i<=n;i++) ans=(ans+g[n][i])%MOD;
for(register int c=1;c<=m;c++){
memset(f,0,sizeof(f));
f[0][n]=1;
for(register int i=1;i<=n;i++){ //枚举行
for(register int j=n-i;j<=n+i;j++){ //由于还有可能比其他列少,所以以n为零点
f[i][j]=((f[i-1][j]+f[i-1][j-1]*a[i][c]%MOD)%MOD+f[i-1][j+1]*(sum[i]-a[i][c])%MOD)%MOD;
// cout<<a[i][j]<<endl;
// cout<<f[i-1][j]<<" "<<f[i-1][j-1]*a[i][j]<<" "<<f[i-1][j+1]*(sum[i]-a[i][j])<<"\n";
// cout<<f[i][j]<<"\n";
}
}
for(register int i=n+1;i<=2*n;i++) ans=(ans-f[n][i]+MOD)%MOD;
}
printf("%lld\n",ans);
return 0;
}