[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\)

转移也很明显是:

\[g[i][j]=g[i-1][j]+g[i-1][j-1]\times sum[i] \]

即两种情况:

  • \(i\) 行选。

  • \(i\) 行不选。

对于第 \(i\) 行选,\(sum[i]\) 表示第 \(i\) 行菜肴的总数量,即:

\[sum[i]=\sum_{j=1}^{j<=m} a[i][j] \]

而我们只考虑前两个限制条件的答案此时也是呼之欲出:

\[ans=\sum_{i=1}^{i<=n} g[n][i] \]

\(1\) 开始时因为限制条件 \(1\) ,只到 \(n\) 则是由于限制二所以我们最多选择 \(n\) 道菜。

到这里,我们就算做出了前两个限制条件的情况,即我们的 \(ans\)

第三个限制

做到这里,我们想要采取的方法也是很明显了,就是想要做一个容斥,从 \(ans\) 里面减去不合法方案。

怎么求不合法方案呢?有一个常见的套路是,我们可以固定一列,令其不合法。

到这里其实我们已经和行没有太大的关系,所以我们考虑枚举列 \(c\)

\(f[i][j][k]\) 表示前 \(i\)\(c\) 列选择了 \(j\) 个,其他列选择了 \(k\) 个。

转移和我们之前 \(g\) 的转移也是非常的像:

\[f[i][j][k]=f[i-1][j][k]+f[i-1][j-1][k]\times a[i][c]+f[i-1][j][k-1]\times (sum[i]-a[i][c]) \]

讨论的其实也是两种情况,只不过对于第 \(i\) 行要选,又分了两种:

  • \(i\) 行选。

    • 选在第 \(c\) 列。

    • 选在其他列。

  • \(i\) 行不选。

求出我们第 \(c\) 列的 \(f\) 数组后,我们要容斥掉哪些情况?

很显然我们只需要容斥掉 \(j>k\) 的情况即可。

则:

\[ans-\sum_{c=1}^{c\leq m}\sum_{j=1}^{j\leq n}\sum_{k=0}^{k<j} f[n][j][k] \]

将近 \(O(mn^2)\) 的复杂度 ,显然是不可过的,考虑优化。

我们发现我们关心的不是 \(j\)\(k\) 的具体数值大小,而是它们之间的大小关系,后面的两维可以合成一维。

\(f[i][j]\) 表示前 \(i\) 行中,第 \(c\) 列比其他列多选 \(j\) 道菜。

我们也能够很轻易的写出状态转移:

\[f[i][j]=f[i-1][j]+f[i-1][j-1]\times a[i][c]+f[i-1][j+1] \times (sum[i]-a[i][c]) \]

其实和三维的状况是一样的,这里就不赘述了。

说说需要注意的地方吧,第二维上说比其他列多选,其实有可能多选“负数”道菜,什么意思呢?有可能少选,于是我们这里要用上一个技巧,将 \(n\) 看做是零,即 \(n+1\) 看做是多做 \(1\) 道菜,那 \(n-1\) 也就是少做 \(1\) 道菜,到这里也就差不多结束了,最后的答案是:

\[ans-\sum_{c=1}^{c\leq m} \sum_{j=1}^{j\leq n} f[n][j] \]

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;
}
posted @ 2021-08-20 11:43  ╰⋛⋋⊱๑落叶๑⊰⋌⋚╯  阅读(60)  评论(0编辑  收藏  举报