【题解】Emiya家今天的饭
Emiya家今天的饭
看到题目,第一反应当然是拿部分分
-
Part1.for循环(\(8\)pts)
当\(n==2\)时比较简单,所以考试时就拿了这八分我们先从\(n==2\)入手。我们可以来枚举所有组合,并选择符合要求的方案。
8pts code
#include<cstdio>
#include<iostream>
using namespace std;
int m,n;
long long cnt;
int a[150][2500];
const long long mod=998244353;
int main()
{
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<=m;i++)
{
if(!a[1][i]) continue;
for(int j=1;j<=m;j++)
{
if(a[2][j]&&i!=j)
{
cnt+=a[1][i]*a[2][j];
cnt%=mod;
}
}
}
printf("%lld",cnt%mod);
return 0;
}
-
part2.dfs暴搜(32pts)
枚举每种方法和食材
dfs参数含义:
x:烹饪方法;
tot:当前菜共用了多少次;
sum:方案总数;
易得:该方法不选时,进入下一种方法\((x+1)\),\(sum\)不变,\(tot\)不变
32pts code
#include<cstdio>
#include<iostream>
using namespace std;
int m,n;
int num[105][2005];
long long ans;
//n 烹饪方法 m 食材
//num[i][j] i个烹饪方法 j个食材
const int mod=998244353;
int cnt[2500];
void dfs(int x,int tot,long long sum)
//x 烹饪方法
{
if(x>n) //所有烹饪方法都用过了
{
if(tot>1)
{
for(int j=1;j<=m;j++)
if(cnt[j]>tot/2) return;//超过一半
ans+=sum,ans%=mod;
}
return;
}
dfs(x+1,tot,sum);//这个烹饪方法不选菜
for(int i=1;i<=m;i++)
if(num[x][i])
{
cnt[i]++;
dfs(x+1,tot+1,sum*num[x][i]%mod);//该烹饪方法选一道菜
cnt[i]--;
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&num[i][j]);
dfs(1,0,1);
printf("%lld",ans);
return 0;
}
-
Part3.DP(84pts)
做完32分,我们发现正面做比较困难...
正难则反我们可以尝试从反面解决这个问题。
前i-1种烹饪方法中共有x种选法,第i种烹饪方法共y种食材,则前i种烹饪方法共有几种选法?\(x*y\)?恭喜你,答错了。如果这样,那么会有两个问题。第一:若有一种烹饪方法一道菜都不会做,会发生什么?会使所有方法清零。第二:该方法没有选菜的情况未被包含在内。所以前i种烹饪方法共有\(x*(y+1)\)种选法。
值得注意的是:最后要把ans--,去掉什么都不选的情况。
我们需要先求出所有情况,然后再判断是否非法。
用\(dp[i][j][k][p]\)表示前\(i\)种烹饪方法,前\(j\)种食材,共做\(k\)道菜,其中有\(p\)道为第\(j\)种食材做的。当\(p*2>k\)时,该方案非法。
tip:状态转移时,\(k,p\)都要从\(0\)开始,\(k-1\)和\(p-1\)可能为负数。为防止这种情况导致的数组越界,可以采用坐标平移法,把\(k\)和\(p\)统一加一个数\(N\)
可以得到:
dp[i][j][k+N][p+N]=dp[i-1][j][k+N][p+N];//不选这种烹饪方法
dp[i][j][k+N][p+N]+=dp[i-1][j-1][k+N-1][p+N-1]*num[i][j];//选这种烹饪方法的这种食材
dp[i][j][k+N][p+N]+=dp[i-1][j][k+n-1][p+N]*(sum[i]-num[i][j]);//选这种烹饪方法,但不是这种食材
84pts code
#include<cstdio>
#include<iostream>
using namespace std;
const int N=2;
int m,n;
long long dp[45][505][45][45];
int sum[45];
int num[41][505];
const int mod = 998244353;
long long ans=1;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
scanf("%d",&num[i][j]),sum[i]+=num[i][j];
ans=(sum[i]+1)*ans%mod;
}
ans--;
for(int i=1;i<=m;i++)
dp[1][i][0+N][0+N]=1,dp[1][i][1+N][1+N]=num[1][i],dp[1][i][1+N][0+N]=sum[1]-num[1][i];
for(int i=2;i<=n;i++)
for(int j=1;j<=m;j++)
for(int k=0;k<=i;k++)
for(int p=0;p<=k;p++)
{
dp[i][j][k+N][p+N]=((dp[i-1][j][k+N][p+N]+dp[i-1][j][k-1+N][p-1+N]*num[i][j]%mod)%mod+dp[i-1][j][k-1+N][p+N]*(sum[i]-num[i][j]))%mod;
if(p*2-k>0&&i==n)
ans-=dp[i][j][k+N]p+N],ans%=mod;
}
printf("%lld",(ans+mod)%mod);
return 0;
}
p.s. 该方法比较玄学,同一份代码不同时间提交得分不同....
AC代码来了
-
part4. DP++(AC)
我们注意到,上述代码可以利用滚动数组滚掉\(i\)一维
我们还注意到,上述方法,我们只需关心\(p*2-k_0\)(为避免混淆,在本部分,part3的\(k\)记为\(k_0\))的值。为防止出现负数,我们可以故技重施采用相同方法——坐标平移。
\(dp[j][k]\)表示:前\(j\)种食材,\(p*2-k_0\)
可以得到状态转移方程:
dp[j][k]=dp[j][k];//不选这个烹饪方法
dp[j][k]+=dp[j-1][k-1]*num[i][j];//选这种烹饪方法的这种食材(k项展开为:(p-1)*2-(k0-1))
dp[j][k]+=dp[j-1][k+1]*(num[i][0]-num[i][j]);//选这种烹饪方法,但不是这种食材(同上)
AC code
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int N=1e2+5,M=2e3+5;
const int mod=998244353;
int n,m;
long long a[N][M],dp[N][N<<1];
long long ans=1;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
scanf("%lld",&a[i][j]);
a[i][0]+=a[i][j],a[i][0]%=mod;//a[i][0]取代sum[i]省空间(虽然没什么用...)
}
ans*=(a[i][0]+1),ans%=mod;
}
ans--;
for(int i=1;i<=m;i++)
{
memset(dp,0,sizeof(dp));
dp[0][n]=1;// n :坐标平移
for(int j=1;j<=n;j++)
for(int k=n-j;k<=n+j;k++)
dp[j][k]=((dp[j-1][k]+dp[j-1][k-1]*a[j][i])%mod+dp[j-1][k+1]*(a[j][0]-a[j][i]))%mod;
for(int j=n+1;j<=n*2;j++)//k>n<==>2*p-k>0<==>p*2>k<==>不合法
ans-=dp[n][j],ans=(ans%mod+mod)%mod;
}
printf("%lld",(ans+mod)%mod);
return 0;
}
然后就愉快地AC了!
浙公网安备 33010602011771号