先算每种做法任选或不选的总方案数,再按食材枚举严格多数者,用差值 DP 统计坏方案并从总数中扣掉。
OJ: luogu
题目 ID: P5664
难度:提高+/省选-
标签:动态规划容斥组合计数计数dp思维
创建: 2026-06-20 07:35
更新: 2026-10-07 10:45
形式化题目
给定一个
要求选出一个非空方案,满足:
- 每种烹饪方法至多选一道菜(每行至多选一个)
- 设最终选了
道菜,则每种食材出现的次数都不超过
统计所有合法方案数,对
暴力解法
思路
逐行枚举:对第
枚举完所有行后,检查是否存在某种食材出现次数严格超过一半。若没有,则把该方案的方案数累入答案。
代码
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-06-20 07:35
* update_at: 2026-10-01 20:09
*/
// brute.cpp:小数据暴力解,逐行枚举"不选"或"选某一种食材",
// 到叶子节点检查是否存在某种食材严格超过一半。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 15;
const int MAXM = 15;
const ll MOD = 998244353LL;
int n, m;
int a[MAXN][MAXM]; // 输入矩阵
int cnt[MAXM]; // cnt[j]:当前方案中食材 j 被选了多少次
ll ans;
// 模意义下加法
void add_mod(ll &x, ll y) {
x += y;
if (x >= MOD) {
x -= MOD;
}
}
// 检查当前方案是否合法:没有任何食材严格超过一半
bool check(int chosen) {
for (int j = 1; j <= m; j++) {
if (cnt[j] * 2 > chosen) {
return false;
}
}
return true;
}
// 逐行枚举:第 row 行要么不选,要么选食材 j(j 从 1 到 m)
void dfs(int row, int chosen, ll ways) {
if (row > n) {
if (chosen == 0) {
return;
}
if (check(chosen)) {
add_mod(ans, ways);
}
return;
}
// 不选第 row 行
dfs(row + 1, chosen, ways);
// 选第 row 行的食材 j
for (int j = 1; j <= m; j++) {
if (a[row][j] == 0) {
continue;
}
cnt[j]++;
dfs(row + 1, chosen + 1, ways * a[row][j] % MOD);
cnt[j]--;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
}
}
dfs(1, 0, 1);
cout << ans << '\n';
return 0;
}复杂度
- 时间:
,每行有"不选"和"选食材 "共 种选择 - 空间:
,只记录每种食材被选次数
瓶颈
枚举量随
正解
思路
先不考虑"每种食材不能超过一半"的限制,计算总方案数,再扣掉坏方案(存在某种食材严格超过一半)。
总方案数:第
减一是去掉空方案。
坏方案计数:关键观察是——严格多数的食材至多只有一种。若两种食材都超过一半,则它们的出现次数之和大于总菜数,矛盾。
因此可以枚举哪一种食材
固定食材 后的三种选择
把每一行的选择分成三类:
| 对第 |
方案数 | 差值变化 |
|---|---|---|
| 不选 | ||
| 选食材 |
||
| 选其它食材 |
定义
最终
于是做一维 DP:
表示当前差值为 的方案数( 用偏移量 映射到非负下标) - 每一行按上表做三种转移
- 最后把所有
的状态加起来,就是"食材 成为严格多数"的坏方案数
把所有食材的坏方案数加起来,从总方案数里减掉即得答案。
代码
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-06-20 07:35
* update_at: 2026-10-01 20:09
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 105;
const int MAXM = 2005;
const int MAXD = MAXN * 2 + 5;
const ll MOD = 998244353LL;
int n, m;
int a[MAXN][MAXM]; // a[i][j]:第 i 种做法用食材 j 的方案数,值域 < MOD,用 int
ll row_sum[MAXN]; // 第 i 行所有食材方案数之和(模 MOD)
ll dp[MAXD]; // dp[d]:当前差值为 d 的方案数,d 用偏移量 offset = n+1 映射到非负下标
ll ndp[MAXD]; // 下一行滚动数组
// 模意义下加法:x = (x + y) % MOD
void add_mod(ll &x, ll y) {
x += y;
if (x >= MOD) {
x -= MOD;
}
}
// 统计"食材 food 成为严格多数"的坏方案数。
// diff = 选 food 的次数 - 选其它食材的次数,
// 最终 diff > 0 表示 food 严格超过一半。
ll count_bad_for_food(int food) {
int offset = n + 1;
int left = offset;
int right = offset;
memset(dp, 0, sizeof(dp));
dp[offset] = 1;
for (int i = 1; i <= n; i++) {
memset(ndp, 0, sizeof(ndp));
ll same = a[i][food]; // 选食材 food 的方案数
ll other = row_sum[i] - same; // 选非 food 食材的方案数
if (other < 0) {
other += MOD;
}
for (int d = left; d <= right; d++) {
ll cur = dp[d];
if (cur == 0) {
continue;
}
// 第 i 种做法不选。
add_mod(ndp[d], cur);
// 选一个食材是 food 的菜,差值 +1。
if (same != 0) {
add_mod(ndp[d + 1], cur * same % MOD);
}
// 选一个食材不是 food 的菜,差值 -1。
if (other != 0) {
add_mod(ndp[d - 1], cur * other % MOD);
}
}
left--;
right++;
memcpy(dp, ndp, sizeof(dp));
}
ll ans = 0;
for (int d = offset + 1; d <= right; d++) {
add_mod(ans, dp[d]);
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
row_sum[i] += a[i][j];
if (row_sum[i] >= MOD) {
row_sum[i] -= MOD;
}
}
}
// 不考虑"某种食材不能超过一半"时:
// 每种做法要么不选,要么任选一种食材做一道菜。
ll total = 1;
for (int i = 1; i <= n; i++) {
total = total * (row_sum[i] + 1) % MOD;
}
total = (total - 1 + MOD) % MOD; // 去掉空方案
// 枚举哪一种食材成为"严格多数",把所有坏方案扣掉。
ll bad = 0;
for (int food = 1; food <= m; food++) {
add_mod(bad, count_bad_for_food(food));
}
ll ans = (total - bad) % MOD;
if (ans < 0) {
ans += MOD;
}
cout << ans << '\n';
return 0;
}复杂度
- 时间:
,读入 ,枚举 种食材各做一次 的 DP - 空间:
,输入矩阵占主要空间,DP 数组
总结
这题最关键的两步是:
- 把"每种食材都不能超过一半"改写成"不能存在严格多数食材"
- 固定一个候选多数食材后,把所有其它食材合并成一类,只维护两边数量差
一旦做到这一步,原本看起来像多维计数的问题,就被压成了一个很规整的一维 DP。
图示解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
