先算每种做法任选或不选的总方案数,再按食材枚举严格多数者,用差值 DP 统计坏方案并从总数中扣掉。
OJ: luogu
题目 ID: P5664
难度:提高+/省选-
标签:动态规划容斥组合计数计数dp思维
日期: 2026-06-20 07:35
题意
我们要从这些菜里选出一个方案。
因为同一种烹饪方法不能重复,所以每一行最多选一道菜;如果选了第 i 行的第 j 列,就有 a[i][j] 种不同的具体菜可选。
设最终一共选了 k 道菜,那么题目的限制是:每种食材出现次数都不能超过 floor(k/2)。
换句话说,不能存在某种食材严格超过一半。
需要统计所有合法方案数,答案对 998244353 取模。
思路
先看一个可以直接验证想法的朴素解:
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int MAXN = 15;
const int MAXM = 15;
const i64 MOD = 998244353LL;
int n, m;
int a[MAXN][MAXM];
int cnt[MAXM];
i64 ans;
// 朴素暴力:
// 每种做法枚举“不选”或“选哪一种食材”,
// 到叶子时再检查是否存在某种食材超过了一半。
void dfs(int row, int chosen, i64 ways) {
if (row > n) {
if (chosen == 0) {
return;
}
for (int j = 1; j <= m; j++) {
if (cnt[j] * 2 > chosen) {
return;
}
}
ans += ways;
ans %= MOD;
return;
}
dfs(row + 1, chosen, ways);
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;
}这个暴力会逐行枚举:
- 不选这一行
- 或者选这一行的某一种食材
最后统计每种食材出现次数,检查是否合法。它适合做小数据对拍,但显然不能处理 n=100,m=2000。
所以我们换个角度:先数总方案,再扣掉坏方案。
不考虑“某种食材不能超过一半”时,第 i 行有:
1种不选sum_j a[i][j]种选法
因此总方案数就是:
prod_i (1 + sum_j a[i][j]) - 1
这里减一是去掉空方案。
接下来只需要统计坏方案:也就是存在某种食材 p 出现次数严格大于一半。
注意一个关键事实:严格多数的食材至多只有一种。
因为如果两种食材都超过一半,它们的出现次数和就会大于总菜数,矛盾。
所以可以直接枚举哪一种食材是“严格多数者”,分别计数后求和。
固定一种食材后的三种选择
这张表把“固定食材 p 后的一行转移”压成了三类:
对第 i 行的处理 |
方案数 | 差值 diff 的变化 |
|---|---|---|
| 不选 | 1 |
0 |
选食材 p |
a[i][p] |
+1 |
| 选其它食材 | sum_j a[i][j] - a[i][p] |
-1 |
其中我们定义:
diff = 选了多少道食材 p - 选了多少道其它食材
最后只要 diff > 0,就说明食材 p 比其它所有食材加起来还多,也就是它严格超过了一半。
于是可以做一维 DP:
dp[d]表示当前差值为d的方案数- 每一行按上表做三种转移
- 最后把所有
diff > 0的状态加起来,就是“食材p成为严格多数”的坏方案数
把所有食材的坏方案数加起来,再从总方案数里减掉,就是答案。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int MAXN = 105;
const int MAXM = 2005;
const int MAXD = MAXN * 2 + 5;
const i64 MOD = 998244353LL;
int n, m;
int a[MAXN][MAXM];
int row_sum[MAXN];
i64 dp[MAXD], ndp[MAXD];
void add_mod(i64 &x, i64 y) {
x += y;
if (x >= MOD) {
x -= MOD;
}
}
i64 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));
int same = a[i][food];
int other = row_sum[i] - same;
if (other < 0) {
other += MOD;
}
for (int d = left; d <= right; d++) {
i64 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));
}
i64 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;
}
}
}
// 不考虑“某种食材不能超过一半”时:
// 每种做法要么不选,要么任选一种食材做一道菜。
i64 total = 1;
for (int i = 1; i <= n; i++) {
total = total * (row_sum[i] + 1LL) % MOD;
}
total = (total - 1 + MOD) % MOD; // 去掉空方案
// 枚举哪一种食材成为“严格多数”,把所有坏方案扣掉。
i64 bad = 0;
for (int food = 1; food <= m; food++) {
add_mod(bad, count_bad_for_food(food));
}
i64 ans = (total - bad) % MOD;
if (ans < 0) {
ans += MOD;
}
cout << ans << '\n';
return 0;
}复杂度
- 枚举行和列读入:
- 枚举每种食材做差值 DP:
- 空间复杂度:
总结
这题最关键的两步是:
- 把“每种食材都不能超过一半”改写成“不能存在严格多数食材”
- 固定一个候选多数食材后,把所有其它食材合并成一类,只维护两边数量差
一旦做到这一步,原本看起来像多维计数的问题,就被压成了一个很规整的一维 DP。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
