【題解】[CSP-S2019] Emiya 家今天的飯

[CSP-S2019] Emiya 家今天的飯

\(\text{Solution:}\)

又是一個經典題目……一直都不太會 肝了兩天算是搞明白了

首先,觀察到題目限制應該不難想到一個容斥。因爲思考一下發現這兩個限制同時滿足難以表達在狀態裏,因爲我們不能對每一道食材都記錄它的出現次數。同時還有 至少 這個詞彙。那麼對誰容斥?

那應該就是對 不超過一半 的這個限制容斥了。現在讓我們枚舉一道食材,讓它不滿足這個限制,也就是說 強制出現次數大於 \(\frac{n}{2}\) 次。

那我們又可以發現,剩下的食材無論怎麼選擇都不會再出現第二個不合法的了!

那麼最終的方案數得以輕鬆表示:用總方案數減掉每一道菜不合法的方案數。

現在的問題就轉化爲一個求不合法方案數的問題。

那麼,設 \(f[i][j][k]\) 表示前 \(i\) 種方法,選擇了 \(j\) 種,其中有 \(k\) 種是不合法食材做的菜的搭配方案數。

那麼就會有一個簡單的 \(dp:\)

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

其中,\(sum[i]\) 表示這一種方法所能做的菜的總數, \(a[i][pos]\) 表示用這種方法與 \(pos\) 食材所能做的菜的種類數。

於是上面的方程就分別對應了:不選,選擇一個不合法食材,選擇一個合法食材的三種轉移。

初始值:\(f[i][0][0]=1,\) 轉移的時候不要忘記轉移 \(f[i][j][0]!!\)

#include<bits/stdc++.h>
using namespace std;
const int N=101;
int f[N][N][N],a[N][2001];
int sum[N],n,m;
const int mod=998244353;
inline int Add(int x,int y){return (x+y)%mod;}
inline int Mul(int x,int y){return 1ll*x*y%mod;}
inline int Max(int x,int y){return x>y?x:y;}
inline int Min(int x,int y){return x<y?x:y;}
inline int Dec(int x,int y){return (x-y+mod)%mod;}
int main(){
	freopen("in.txt","r",stdin);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i){
		for(int j=1;j<=m;++j){
			scanf("%d",&a[i][j]);
		}
	}	
	for(int i=1;i<=n;++i){
		for(int j=1;j<=m;++j){
			sum[i]=Add(sum[i],a[i][j]);
		}
	}
	int All=1;
	for(int i=1;i<=n;++i)All=Mul(All,sum[i]+1);
	for(int pos=1;pos<=m;++pos){
		memset(f,0,sizeof f);
		f[0][0][0]=1;
		for(int i=1;i<=n;++i){
			f[i][0][0]=1;
			for(int j=1;j<=n;++j){
				f[i][j][0]=Add(f[i-1][j][0],Mul(f[i-1][j-1][0],sum[i]-a[i][pos]));////f[i][j][0]!
				for(int k=1;k<=n;++k){
					f[i][j][k]=Add(f[i][j][k],f[i-1][j][k]);
					f[i][j][k]=Add(f[i][j][k],Mul(f[i-1][j-1][k-1],a[i][pos]));
					f[i][j][k]=Add(f[i][j][k],Mul(f[i-1][j-1][k],sum[i]-a[i][pos]));
				}
			}
		}
		for(int i=0;i<=n;++i){
			for(int j=(i/2)+1;j<=n;++j)
				All=Dec(All,f[n][i][j]);
		}
	}
	printf("%d\n",All-1);
	return 0;
}

這樣就有了 \(84pts\)

繼續考慮,把 \(O(n^3m)\) 優化到 \(O(n^2m)\) 就完事了。

觀察一下,我們能不能把 選擇多少道菜 和 選了多少道不合法的菜 合併到一起?

仔細想想,如果我們知道在一種選擇方案下,選擇的 \(pos\) 菜品的數量比其他的更多,那麼我們實際上無需知道一共選擇了多少個,只需要知道這種情況的方案數然後減掉即可。

那麼我們就可以考慮換一個狀態:設 \(f[i][j]\) 表示前 \(i\) 種食材,選擇了 \(pos\) 食材的食品數量與其他的食材食品數量之差爲 \(j\) 的方案數。

注意這裏的 \(j\) 可以是負數,所以要整體平移。

那麼就可以同樣地寫出方程:

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

初始值也是一樣的,而這裏注意一下減的時候要從 \(pos\) 數目嚴格大於的時候開始算。

如果 \(j=0\) 實際是合法方案數。

這樣少掉一層循環,複雜度就是 \(O(n^2m)\) 了。

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=501;
int f[N][N],a[N][5001];
int sum[N],n,m;
const int mod=998244353;
inline int Add(int x,int y){return (x+y)%mod;}
inline int Mul(int x,int y){return 1ll*x*y%mod;}
inline int Max(int x,int y){return x>y?x:y;}
inline int Min(int x,int y){return x<y?x:y;}
inline int Dec(int x,int y){return (x-y+mod)%mod;}
inline int getpos(int x){return (x+n+1);}
void print(int P){
	printf("%d:\n",P);
	for(int i=0;i<=n;++i)
		for(int j=-n;j<=n;++j)
			printf("%d%c",f[i][getpos(j)],j==n?'\n':' ');
	puts("");
}
signed main(){
	freopen("meal.in","r",stdin);
	freopen("meal.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i){
		for(int j=1;j<=m;++j){
			scanf("%d",&a[i][j]);
		}
	}	
	for(int i=1;i<=n;++i){
		for(int j=1;j<=m;++j){
			sum[i]=Add(sum[i],a[i][j]);
		}
	}
	int All=1;
	for(int i=1;i<=n;++i)All=Mul(All,sum[i]+1);
	for(int pos=1;pos<=m;++pos){
		memset(f,0,sizeof f);
		f[0][n+1]=1;
		for(int i=1;i<=n;++i){
			for(int j=-n;j<=n;++j){
				int poss=getpos(j);
				f[i][poss]=Add(f[i-1][poss],f[i][poss]);
				f[i][poss]=Add(f[i][poss],Mul(f[i-1][poss-1],a[i][pos]));
				f[i][poss]=Add(f[i][poss],Mul(f[i-1][poss+1],(sum[i]-a[i][pos])));
			}
		}
		for(int i=1;i<=n;++i)All=Dec(All,f[n][getpos(i)]);
	}
	printf("%lld\n",All-1);
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章