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 @   dddddadaplllllane  阅读(41)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示