ARTICLE · 1133880
真题解析:P5664 [CSP-S 2019] Emiya 家今天的饭(容斥补集·超标食材唯一性·差值DP·滚动数组)
题目来源:洛谷 P5664 [CSP-S 2019] Emiya 家今天的饭
题目大意
Emiya 掌握 n 种烹饪方法、会使用 m 种主要食材。用第 i 种烹饪方法和第 j 种主要食材能做出 a[i][j] 道不同的菜。一道菜恰好由一种烹饪方法和一种主要食材做成。
现在要选出一个包含 k 道菜(k ≥ 1)的搭配方案,满足:
• 每道菜的烹饪方法互不相同(即每种烹饪方法至多贡献一道菜); • 每种主要食材在所选菜中出现的次数不超过 ⌊k/2⌋。
求满足条件的搭配方案数,对 998244353 取模。
数据范围:n ≤ 100,m ≤ 2000,0 ≤ a[i][j] ≤ 100(答案模 998244353,是质数)。
输入输出样例
2 31 0 10 1 133 31 2 34 5 06 0 0190样例 1 中每种烹饪方法至多做一道菜,合法方案恰好 3 种;样例 2 答案为 190。
考点梳理
解题思路
一、先算「烹饪方法互不相同」的全部方案
只保留「每道菜烹饪方法互不相同」这一个条件时,每种烹饪方法 i 的选择是独立的:不做菜有 1 种,做一道菜有 s[i] = Σ a[i][j] 种。由乘法原理,允许全不做的总方案数为:
all = ∏(1 + s[i]) − 1
减去的 1 是「所有烹饪方法都不做菜」的空方案(题目要求 k ≥ 1)。
二、用容斥扣掉「某种食材超过半数」的非法方案
关键在于一个结构性质:超标的食材至多只有一种。设总菜数为 k,若食材 A、B 都超过 k/2,则二者之和大于 k,与总菜数恰为 k 矛盾。
所以非法方案里恰好有一种食材超标,不同食材对应的非法集合两两不相交。我们可以枚举超标食材 c,分别统计「食材 c 超标」的方案数 bad[c],直接相加即为全部非法方案数。
三、固定食材 c,用差值 DP 统计超标方案
判断食材 c 是否超标,并不需要记录每种食材各做了多少,只需一个差值:
d =(用食材 c 的菜数)−(用其它食材的菜数)
总菜数为 k 时,「c 超过一半」等价于 count[c] > k − count[c],即 d > 0。
设 dp[d] 表示处理完若干烹饪方法后差值为 d 的方案数,差值范围为 [−n, n],存数组时统一加偏移量 n,初始 dp[0] = 1。对第 i 种烹饪方法,有三种转移:
• 不做菜:差值不变,方案数乘 1; • 做食材 c 的菜:差值加一,方案数乘 a[i][c]; • 做其它食材的菜:差值减一,方案数乘 s[i] − a[i][c]。
处理完所有方法后,所有 d > 0 的 dp[d] 之和就是 bad[c]。
四、汇总答案
把每个食材的 bad[c] 累加成 invalid,最终:
ans = (all − invalid) mod 998244353
值得注意的是,用差值 d 这一个数就完整表达了「是否超标」所需的全部信息,从而让每种食材的统计都能在 O(n²) 内完成。
参考代码
#include <bits/stdc++.h>using namespace std;const long long MOD = 998244353;const int N = 105, M = 2005;int n, m;long long a[N][M];long long s[N];long long dp[2 * N + 5];long long ndp[2 * N + 5];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][j] %= MOD; s[i] = (s[i] + a[i][j]) % MOD; } // 全部非空方案:prod(1 + s[i]) - 1 long long total = 1; for (int i = 1; i <= n; i++) total = total * ((1 + s[i]) % MOD) % MOD; total = (total - 1 + MOD) % MOD; // 减去不合法的:某种食材出现次数超过一半 long long invalid = 0; int off = n; for (int c = 1; c <= m; c++) { memset(dp, 0, sizeof(dp)); dp[off] = 1; for (int i = 1; i <= n; i++) { long long ac = a[i][c]; long long other = (s[i] - ac + MOD) % MOD; memset(ndp, 0, sizeof(ndp)); for (int d = -i; d <= i; d++) { int idx = off + d; long long v = dp[idx]; if (v == 0) continue; ndp[idx] = (ndp[idx] + v) % MOD; if (idx + 1 <= 2 * off) ndp[idx + 1] = (ndp[idx + 1] + v * ac) % MOD; if (idx - 1 >= 0) ndp[idx - 1] = (ndp[idx - 1] + v * other) % MOD; } memcpy(dp, ndp, sizeof(dp)); } for (int d = 1; d <= n; d++) invalid = (invalid + dp[off + d]) % MOD; } long long ans = (total - invalid + MOD) % MOD; printf("%lld\n", ans); return 0;}复杂度分析
• 时间:外层枚举超标食材 O(m),每个食材内做 O(n) 种方法、每种方法枚举 O(n) 个差值,单食材 O(n²),总复杂度 O(n²m)。n ≤ 100、m ≤ 2000 时约为 2 × 10⁷ 次操作。 • 空间:滚动数组只需 O(n) 存差值状态,加上 a 矩阵(105 × 2005)与 s 数组,整体约 O(nm + n)。
推荐阅读
从暴力枚举到算法策略:CSP-J 2021插入排序真题实战解析
真题解析:P11232 [CSP-S 2024] 超速检测(运动学公式·二分映射·区间覆盖贪心)
真题解析:P8818 [CSP-S 2022] 策略游戏(博弈论·分类讨论·ST表区间查询)
真题解析:P7913 [CSP-S 2021] 廊桥分配(贪心·优先队列·前缀和)
真题解析:P7915 [CSP-S 2021] 回文(贪心·双端队列·回文构造)
真题解析:P7075 [CSP-S 2020] 儒略日(模拟·日期计算·闰年判断)
真题解析:P5658 [CSP-S 2019] 括号树(模拟·树形DP·栈)
真题解析:P11230 [CSP-J 2024] 接龙(子序列匹配·状态递推·分类讨论)
真题解析:P14362 [CSP-S 2025] 道路修复(最小生成树·子集枚举·并查集·多路归并)
真题解析:P11233 [CSP-S 2024] 染色(线性DP·前缀和·桶数组·同色段·最近出现位置)
真题解析:P9753 [CSP-S 2023] 消消乐(栈模拟·多项式哈希·双哈希·组合计数)
真题解析:P8817 [CSP-S 2022] 假期计划(BFS最短路·枚举优化·候选集剪枝·64位整数)
真题解析:P9754 [CSP-S 2023] 结构体(模拟·内存对齐·嵌套结构体·地址映射)
真题解析:P9755 [CSP-S 2023] 种树(二分答案·等差数列求和·树上逆向贪心·优先队列)
真题解析:P7914 [CSP-S 2021] 括号序列(区间DP·唯一分解去重·组合计数·前缀和优化)
真题解析:P7077 [CSP-S 2020] 函数调用(拓扑排序·逆序扫描·乘法标记下传·线性变换)
我是欣爸,中学开始学习编程,计算机专业毕业,从事互联网行业软件开发20余年。热爱编程,热爱算法,孩子也很喜欢数学、编程,业余时间辅导孩子学习编程、算法,分享编程算法学习、信奥竞赛经验。