P5664 [CSP-S2019] Emiya 家今天的饭 题解
题目大意
再每一组里面有很多种方案,每一行只能选一个
每一列不能选超过总数的一半
思路:
我们看题,不符合贪心,因此这是一道dp
首先考虑每一个状态需要指导那些才能转移:
- 目前选了多少个
- 目前每一列选了多少个
- 目前到了那些列
由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卡了很久
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现