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

这是一篇关于解决组合优化问题的文章,以CSP-S2019题目「Emiya 家今天的饭」为例,讨论如何在满足特定条件(如烹饪方法互不相同,食材不超过一半)下计算不同菜谱方案的数量。文章通过分析题目数据,提出使用动态规划(DP)的方法,并给出了状态转移方程。同时,作者强调了在处理此类问题时要注意的细节,如避免状态空间爆炸和正确处理取模运算。

太难惹!!!


题目描述

Emiya 是个擅长做菜的高中生,他共掌握 n 种烹饪方法,且会使用 m 种主要食材做菜。为了方便叙述,我们对烹饪方法从 1∼n 编号,对主要食材从 1∼m  编号。

Emiya 做的每道菜都将使用恰好一种烹饪方法与恰好一种主要食材。更具体地,Emiya 会做 a_{i,j}​ 道不同的使用烹饪方法 i 和主要食材 j 的菜(1≤i≤n,1≤j≤m),这也意味着 Emiya 总共会做a_{i,j}道不同的菜。

Emiya 今天要准备一桌饭招待 Yazid 和 Rin 这对好朋友,然而三个人对菜的搭配有不同的要求,更具体地,对于一种包含 k 道菜的搭配方案而言:

  • Emiya 不会让大家饿肚子,所以将做至少一道菜,即 k≥1
  • Rin 希望品尝不同烹饪方法做出的菜,因此她要求每道菜的烹饪方法互不相同
  • Yazid 不希望品尝太多同一食材做出的菜,因此他要求每种主要食材至多在一半的菜(即 \left \lfloor \frac{k}{2} \right \rfloor道菜)中被使用

这里的 \left \lfloor \frac{k}{2} \right \rfloor 为下取整函数,表示不超过 x 的最大整数。

这些要求难不倒 Emiya,但他想知道共有多少种不同的符合要求的搭配方案。两种方案不同,当且仅当存在至少一道菜在一种方案中出现,而不在另一种方案中出现。

Emiya 找到了你,请你帮他计算,你只需要告诉他符合所有要求的搭配方案数对质数 998,244,353 取模的结果。

数据范围:

对于所有测试点,保证 1\leq n\leq 100,1\leq m\leq 2000,0\leq a_{i,j}< 998,244,353

一、分析

1.方法

        由题目可知为求解方案数的问题\rightarrow转化为DP/组合数问题。分析题目中的数据 发现给我们提供了大量的状态直接选择DP。通过三个限制条件发现yazid的条件最难完成 所以从yazid 的条件入手,即维护每一列上的菜数不超过总数的一半。但是发现如果进行对于每一列的维护空间时间都不够。由正难则反易得,使用容斥原理,先计算出总体的所有方案数(乘法原理)再计算出每一列不符合的方案数进行差值的计算即可得出答案。

        显然,在一个不成立的列方案中,必有一列的节点大于挑选的总结点,因此可以枚举这个超过限制的列,然后进行DP。

2.状态及状态转移方程

        f[line][j][k]中 i表示已经在这一列枚举到第i行,j表示本列挑选了多少个菜,k表示其他列挑选了多少个菜,对于每个列复杂度为\Theta \left ( n^{3} \right )则总时间复杂度为\Theta \left ( mn^{3} \right )显然已经超过了时间复杂度。

        满分的话还需要进一步优化,对于j和k而言不需要知道具体的数值,由于条件为j> \frac{sum}{2}并且j+k=sum,则只要j-k> 0说明j> \frac{sum}{2}。时间复杂度变为\Theta \left ( mn^{_{2}} \right )

        状态转移方程为f[line][c]=f[line-1][c]+f[line-1][c-1]*a[line][row]+f[line-1][c+1]*(sum[line]-a[line][row])//分别对应1.直接从上一行转移2.从上一行过来在本列选择一个3.从上一行过来在其他列选择。

 3.注意事项

        每次计算列的时候要memset,取模的时候注意不要出现负数,记得开long long!!!           

十年OI一场空,不开longlong见祖宗!

二、代码

#include<iostream>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
#define N 1000010
#define mod 998244353

using namespace std;

ll n,m,ans=1;
ll h[110][2010],cnt[10010],sum[210],f[210][5010];

int main()
{
	scanf("%lld%lld",&n,&m);
	for(ll i=1;i<=n;++i)
	{
		for(ll j=1;j<=m;++j)
		{
			scanf("%lld",&h[i][j]); 
			sum[i]=(sum[i]+h[i][j])%mod;//前缀和求出每行的总数
		}
		ans=(ans*(sum[i]+1))%mod;//计算总方案数
	}
	for(int row=1;row<=m;++row)
	{
		memset(f,0,sizeof(0));
		f[0][n]=1;//重置 为n为了防止负数下标
		for(int line=1;line<=n;++line)
			for(int c=n-line;c<=n+line;++c)//j<k转移到j>k 但都是基于f[0][n]开始
			{
				f[line][c]=((f[line-1][c]+(f[line-1][c-1]*h[line][row])%mod)%mod+f[line-1][c+1]*(sum[line]-h[line][row])%mod)%mod;	
			}
		for(int line=n+1;line<=2*n;++line)
			ans=(ans-f[n][line]+mod)%mod;//计算容斥
                //!!!!取模
	}
	printf("%lld\n",ans-1);
	return 0;
//        华丽结束
}

总结

本文仅仅简单介绍了DP的判断以及推出状态转移方程,但是其他神犇还有更加玄学的做法,详情请移步洛谷。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值