P5664 [CSP-S2019] Emiya 家今天的饭 题解

题目大意

再每一组里面有很多种方案,每一行只能选一个
每一列不能选超过总数的一半

思路:

我们看题,不符合贪心,因此这是一道dp
首先考虑每一个状态需要指导那些才能转移:

  1. 目前选了多少个
  2. 目前每一列选了多少个
  3. 目前到了那些列

由2得
这时候需要状压
但是
m的数据规模为[1,2000]

因此放弃这样设计

于是 正难则反

处理出所有每行只选一个的方案数
再求出每行只选一个的其中一种食材超过一半的方案数
容斥原理

que:是否会有重合重复 的情况

证:
	因为在选择时,不符合的不会有两个都超过了总数的一半的情况;
	即
	不需要减去的有可能重复,
	而要减去的根本不会重复

因此满足

状态设计

我们只需处理出每列超过的就行了
设:

dp[i][j][k]表示目前在第i行,其他的用了j个,这列用了k个

转移

dp[i][j][k]= dp[i][j-1][k]*(s[i]-a[now])
	    +dp[i][j][k-1]*a[now]

这样时间复杂度为O(mn^3)
有80分

暴力代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<climits>
#include<cmath>
#define ll long long

using namespace std;
const int N=101,M=2001;
const ll P=998244353;

int n,m;
ll a[N][M];
ll s[N];
ll dp[N][N][N];
ll f[N][M];

ll res=0,ans=0;

int main() {
	cin>>n>>m;
	for(int i=1;i<=n;i++) {
		for(int j=1;j<=m;j++) {
			cin>>a[i][j];
			s[i]+=a[i][j];
			s[i]%=P;
		}
	}
	f[0][0]=1;
	for(int i=1;i<=n;i++) {
		f[i][0]=1;
		for(int j=1;j<=i;j++) {
			f[i][j]+=(f[i-1][j-1]*s[i])%P+f[i-1][j];
			f[i][j]%=P;
		}
	}
	for(int i=1;i<=n;i++) {
		ans+=f[n][i];
		ans%=P;
	}//所有的
	
	for(int now=1;now<=m;now++) {
		memset(dp,0,sizeof(dp));
		dp[0][0][0]=1;
		for(int i=1;i<=n;i++) {
			for(int j=0;j<=i;j++) {
				for(int k=0;k+j<=i;k++) {
					dp[i][j][k]=dp[i-1][j][k];
					if(j!=0) dp[i][j][k]+=(dp[i-1][j-1][k]*(s[i]-a[i][now]))%P;
					if(k!=0) dp[i][j][k]+=(dp[i-1][j][k-1]*(a[i][now]))%P;
					dp[i][j][k]%=P;
				}
			}
		}
		for(int i=0;i<=n;i++){
			for(int j=0;j+i<=n&&j<i;j++) {
				res+=dp[n][j][i];
				res%=P;
			}
		}
	}//减去的
	
	cout<<((ans+P-res)%P);
	return 0;
}

优化

注意到在不合法情况的计算过程中,
也就是dp[i,j,k]的转移过程中,
我们实际上并不关心j,k的具体数值,
而只关心相对的大小关系

因此只需要看差就行了
加一个n避免负数
复杂度来到O(n^2 m),过

100pts代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<climits>
#include<cmath>
#define ll long long

using namespace std;
const int N=101,M=2001;
const ll P=998244353;

int n,m;
ll a[N][M];
ll s[N];
ll dp[N][2*N];
ll f[N][M];

ll res=0,ans=0;

int main() {
	cin>>n>>m;
	for(int i=1;i<=n;i++) {
		for(int j=1;j<=m;j++) {
			cin>>a[i][j];
			s[i]+=a[i][j];
			s[i]%=P;
		}
	}
	f[0][0]=1;
	for(int i=1;i<=n;i++) {
		f[i][0]=1;
		for(int j=1;j<=i;j++) {
			f[i][j]+=(f[i-1][j-1]*s[i])%P+f[i-1][j];
			f[i][j]%=P;
		}
	}
	for(int i=1;i<=n;i++) {
		ans+=f[n][i];
		ans%=P;
	}
	
	for(int now=1;now<=m;now++) {
		memset(dp,0,sizeof(dp));
		dp[0][n]=1;
		for(int i=1;i<=n;i++) {
			for(int j=n-i;j<=n+i;j++) {
				dp[i][j]=dp[i-1][j];
				if(j!=2*n) dp[i][j]+=(dp[i-1][j+1]*(s[i]-a[i][now]))%P;
				if(j!=0) dp[i][j]+=(dp[i-1][j-1]*(a[i][now]))%P;
				dp[i][j]%=P;
			}
		}
		for(int j=n+1;j<=2*n;j++) {
			res+=dp[n][j];
			res%=P;
		}
	}
	
	cout<<((ans+P-res)%P);
	return 0;
}

总结:

  • 正难则反

当正向解决某个问题的时候,可以用所有解减去不符合要求的解来达成

  • 容斥原理

容斥原理难在于处理交集,要发现本题没有交集

  • 优化状态

技巧:当值不影响结果,而值之间的固定操作(如加减乘除)影响结果时,可以用会影响的来弄

  • 注意取模运算

本题在5pts卡了很久

posted @ 2022-08-09 18:52  dddddadaplllllane  阅读(71)  评论(0)    收藏  举报