题目链接
NOIP2015提高组第二轮 day2 - T1:Emiya 家今天的饭
题目描述
Emiya 是个擅长做菜的高中生,他共掌握 n n n 种烹饪方法,且会使用 m m m 种主要食材做菜。为了方便叙述,我们对烹饪方法从 1 ∼ n 1 \sim n 1∼n 编号,对主要食材从 1 ∼ m 1 \sim m 1∼m 编号。
Emiya 做的每道菜都将使用恰好一种烹饪方法与恰好一种主要食材。更具体地,Emiya 会做 a i , j a_{i,j} ai,j 道不同的使用烹饪方法 i i i 和主要食材 j j j 的菜( 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n、 1 ≤ j ≤ m 1 \leq j \leq m 1≤j≤m),这也意味着 Emiya 总共会做 ∑ i = 1 n ∑ j = 1 m a i , j \sum\limits_{i=1}^{n} \sum\limits_{j=1}^{m} a_{i,j} i=1∑nj=1∑mai,j 道不同的菜。
Emiya 今天要准备一桌饭招待 Yazid 和 Rin 这对好朋友,然而三个人对菜的搭配有不同的要求,更具体地,对于一种包含 k k k 道菜的搭配方案而言:
- Emiya 不会让大家饿肚子,所以将做至少一道菜,即 k ≥ 1 k \geq 1 k≥1
- Rin 希望品尝不同烹饪方法做出的菜,因此她要求每道菜的烹饪方法互不相同
- Yazid 不希望品尝太多同一食材做出的菜,因此他要求每种主要食材至多在一半的菜(即 ⌊ k 2 ⌋ \lfloor \frac{k}{2} \rfloor ⌊2k⌋ 道菜)中被使用
这里的 ⌊ x ⌋ \lfloor x \rfloor ⌊x⌋ 为下取整函数,表示不超过 x x x 的最大整数。
这些要求难不倒 Emiya,但他想知道共有多少种不同的符合要求的搭配方案。两种方案不同,当且仅当存在至少一道菜在一种方案中出现,而不在另一种方案中出现。
Emiya 找到了你,请你帮他计算,你只需要告诉他符合所有要求的搭配方案数对质数 998 , 244 , 353 998,244,353 998,244,353 取模的结果。
输入格式
第 1 行两个用单个空格隔开的整数 n , m n,m n,m。
第 2 行至第 n + 1 n + 1 n+1 行,每行 m m m 个用单个空格隔开的整数,其中第 i + 1 i + 1 i+1 行的 m m m 个数依次为 a i , 1 , a i , 2 , ⋯ , a i , m a_{i,1}, a_{i,2}, \cdots, a_{i,m} ai,1,ai,2,⋯,ai,m。
输出格式
仅一行一个整数,表示所求方案数对 998 , 244 , 353 998,244,353 998,244,353 取模的结果。
样例 #1
样例输入 #1
2 3
1 0 1
0 1 1
样例输出 #1
3
样例 #2
样例输入 #2
3 3
1 2 3
4 5 0
6 0 0
样例输出 #2
190
样例 #3
样例输入 #3
5 5
1 0 0 1 1
0 1 0 1 0
1 1 1 1 0
1 0 1 0 1
0 1 1 0 1
样例输出 #3
742
提示
【样例 1 解释】
由于在这个样例中,对于每组 i , j i, j i,j,Emiya 都最多只会做一道菜,因此我们直接通过给出烹饪方法、主要食材的编号来描述一道菜。
符合要求的方案包括:
- 做一道用烹饪方法 1、主要食材 1 的菜和一道用烹饪方法 2、主要食材 2 的菜
- 做一道用烹饪方法 1、主要食材 1 的菜和一道用烹饪方法 2、主要食材 3 的菜
- 做一道用烹饪方法 1、主要食材 3 的菜和一道用烹饪方法 2、主要食材 2 的菜
因此输出结果为 3 m o d 998 , 244 , 353 = 3 3 \bmod 998,244,353 = 3 3mod998,244,353=3。 需要注意的是,所有只包含一道菜的方案都是不符合要求的,因为唯一的主要食材在超过一半的菜中出现,这不满足 Yazid 的要求。
【样例 2 解释】
Emiya 必须至少做 2 道菜。
做 2 道菜的符合要求的方案数为 100。
做 3 道菜的符合要求的方案数为 90。
因此符合要求的方案数为 100 + 90 = 190。
【数据范围】
测试点编号 | n = n= n= | m = m= m= | a i , j < a_{i,j}< ai,j< | 测试点编号 | n = n= n= | m = m= m= | a i , j < a_{i,j}< ai,j< |
---|---|---|---|---|---|---|---|
1 1 1 | 2 2 2 | 2 2 2 | 2 2 2 | 7 7 7 | 10 10 10 | 2 2 2 | 1 0 3 10^3 103 |
2 2 2 | 2 2 2 | 3 3 3 | 2 2 2 | 8 8 8 | 10 10 10 | 3 3 3 | 1 0 3 10^3 103 |
3 3 3 | 5 5 5 | 2 2 2 | 2 2 2 | 9 ∼ 12 9\sim 12 9∼12 | 40 40 40 | 2 2 2 | 1 0 3 10^3 103 |
4 4 4 | 5 5 5 | 3 3 3 | 2 2 2 | 13 ∼ 16 13\sim 16 13∼16 | 40 40 40 | 3 3 3 | 1 0 3 10^3 103 |
5 5 5 | 10 10 10 | 2 2 2 | 2 2 2 | 17 ∼ 21 17\sim 21 17∼21 | 40 40 40 | 500 500 500 | 1 0 3 10^3 103 |
6 6 6 | 10 10 10 | 3 3 3 | 2 2 2 | 22 ∼ 25 22\sim 25 22∼25 | 100 100 100 | 2 × 1 0 3 2\times 10^3 2×103 | 998244353 998244353 998244353 |
对于所有测试点,保证 1 ≤ n ≤ 100 1 \leq n \leq 100 1≤n≤100, 1 ≤ m ≤ 2000 1 \leq m \leq 2000 1≤m≤2000, 0 ≤ a i , j < 998 , 244 , 353 0 \leq a_{i,j} \lt 998,244,353 0≤ai,j<998,244,353。
算法思想(动态规划)
根据题目描述, Emiya掌握 n n n 种烹饪方法,能够使用 m m m 种主要食材做菜。第 i i i种烹饪方法使用第 j j j种食材,可以做出 a i , j a_{i,j} ai,j 道不同的菜。对于一种包含 k k k 道菜的搭配方案要满足如下条件:
- 至少做一道菜,即 k ≥ 1 k \geq 1 k≥1
- 烹饪方法互不相同,即 k ≤ n k \le n k≤n
- 每种主要食材至多在一半的菜(即 ⌊ k 2 ⌋ \lfloor \frac{k}{2} \rfloor ⌊2k⌋ 道菜)中被使用
直接求符合所有要求的搭配方案数很困难,这里可以采用补集的思想,首先求解满足前 2 2 2个条件的方案总数,然后减去不满足第 3 3 3个条件的方案。
满足前2个条件的方案
下面使用动态规划的思想求解满足前 2 2 2个条件的方案总数:
-
状态
f[i][k]
表示使用前i
种烹饪方法做出k
道菜的方案数,其中 1 ≤ k ≤ n 1\le k\le n 1≤k≤n。方案总数为 f [ n ] [ 1 ] + f [ n ] [ 2 ] + . . . + f [ n ] [ n ] f[n][1]+f[n][2]+...+f[n][n] f[n][1]+f[n][2]+...+f[n][n],即 ∑ k = 1 n f [ n ] [ k ] \sum_{k=1}^nf[n][k] ∑k=1nf[n][k]。 -
状态计算以每种烹饪方法为阶段,对于第
i
种烹饪方法有不使用和使用两种情况:-
不使用第
i
种烹饪方法,那么方案数为f[i-1][k]
-
使用第
i
种烹饪方法,根据不同的食材又可以做 a i , j a_{i,j} ai,j 道不同的菜,其中 1 ≤ j ≤ m 1\le j \le m 1≤j≤m。因此在前i-1
种烹饪方法做出k-1
道菜后,可以分为 m m m种情况:- 使用第
i
种烹饪方法和第1
种食材做出第k
道菜,方案数为f[i-1][k-1] * a[i][1]
- 使用第
i
种烹饪方法和第2
种食材做出第k
道菜,方案数为f[i-1][k-1] * a[i][2]
- …
- 使用第
i
种烹饪方法和第m
种食材做出第k
道菜,方案数为f[i-1][k-1] * a[i][m]
使用第
i
种烹饪方法的方案数为:f[i-1][k-1] * a[i][1] + f[i-1][k-1] * a[i][2] + ... + f[i-1][k-1] * a[i][m]
。不妨设s[i] = a[i][1] + a[i][2] + ... + a[i][m]
,那么方案数为f[i-1][k-1]*s[i]
。 - 使用第
因此状态转移方程
f[i][k] = f[i-1][k] + f[i - 1][k - 1] * s[i]
-
-
初始状态:
f[0][0] = 1
不满足第3个条件的方案
状态表示
第 3 3 3个条件为每种主要食材至多在一半的菜(即 ⌊ k 2 ⌋ \lfloor \frac{k}{2} \rfloor ⌊2k⌋ 道菜)中被使用,也就是说不合法的方案中有且只有一种食材的数量大于其它食材的总和。
这样就用状态 g [ i ] [ u ] [ v ] g[i][u][v] g[i][u][v]表示对于前 i i i种烹饪方法,不合理的食材选了 u u u种,其它食材一共选了 v v v种的方案总数,那么不合理的方案数为 ∑ g [ i ] [ u ] [ v ] ( u > v ) \sum g[i][u][v](u>v) ∑g[i][u][v](u>v)。
通过上述分析可以发现,我们关心的只是不合理的食材数量和其它食材数量的差值。那么不妨设g[i][j]
表示计算前i
种烹饪方法中,使用的不合理的食材数量和其它食材数量的差值为j
的方案数,即可降低g[]
数组的维度。
- 状态表示:
g[i][j]
表示前i
种烹饪方法中,使用的不合理的食材数量和其它食材数量的差值为j
的方案数
状态计算
首先,枚举不合理的食材k
:
- 不选第
i
种烹饪方法的方案数为:g[i - 1][j]
- 选第
i
种烹饪方法的方案数,并且选择不合理的食材k
的方案数:g[i-1][j-1]*a[i][k]
。选择
k
后,差值为j
,那么之前状态的差值为j-1
。 - 选第
i
种烹饪方法的方案数,并且不选择食材k
的方案数:g[i-1][j+1]*s[i]-a[i][k]
不选
k
后,差值为j
,那么之前状态的差值为j+1
。
注意:注意做差可能为负数,因此可以把所有状态加一个偏移量 n n n防止数组越界。
时间复杂度
- 求解满足前2个条件的时间复杂度
- 状态数为 O ( n × n ) O(n\times n) O(n×n)
- 使用
s[i] = a[i][1] + a[i][2] + ... + a[i][m]
优化后,状态计算的时间复杂度为 O ( 1 ) O(1) O(1)
- 求解不满足第3个条件时间复杂度
- 枚举不合理的食材
k
,时间复杂度为 O ( m ) O(m) O(m) - 状态数为 O ( n × n ) O(n\times n) O(n×n)
- 枚举不合理的食材
最终时间复杂度为 O ( n 2 m ) = 2 × 1 0 7 O(n^2m)=2\times10^7 O(n2m)=2×107
代码实现
#include <iostream>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 110, M = 2010, MOD = 998244353;
//对于g数组,加上偏移量n,防止求差数组越界
int a[N][M], s[N], f[N][N], g[N][N * 2];
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
{
cin >> a[i][j];
s[i] = (s[i] + a[i][j]) % MOD;
}
//计算满足前两个条件的方案数
f[0][0] = 1;
for(int i = 1; i <= n; i ++)
for(int k = 0; k <= n; k ++)
{
f[i][k] = f[i - 1][k]; //不使用第i种烹饪方法
if(k > 0)
f[i][k] = (f[i][k] + (LL)f[i - 1][k - 1] * s[i]) % MOD; //使用第i中烹饪方法的方案数
}
LL ans = 0; //求满足前2个条件的方案总数
for(int i = 1; i <= n; i ++)
ans = (ans + f[n][i]) % MOD;
//从总方案数ans中去掉不满足条件3的方案
//枚举不合理的食材k
for(int k = 1; k <= m; k ++)
{
memset(g, 0, sizeof g);
g[0][n] = 1; //加上偏移量n
for(int i = 1; i <= n; i ++)
//枚举食材数量的差值
for(int j = 1; j <= n + i; j ++)
{
g[i][j] = (g[i][j] + g[i - 1][j]) % MOD;
g[i][j] = (g[i][j] + (LL)g[i - 1][j - 1] * a[i][k]) % MOD;
g[i][j] = (g[i][j] + (LL)g[i - 1][j + 1] * (s[i] - a[i][k])) % MOD;
}
//从总方案中减去不满足要求的方案,即差值 > n
for(int i = n + 1; i <= n * 2; i ++)
ans = (ans - g[n][i] + MOD) % MOD; //防止出现负值
}
cout << ans;
return 0;
}