[CSP-S 2019] Emiya 家今天的饭

GitHub跳转原题关系图返回列表

先算每种做法任选或不选的总方案数,再按食材枚举严格多数者,用差值 DP 统计坏方案并从总数中扣掉。

OJ: luogu

题目 ID: P5664

难度:提高+/省选-

标签:动态规划容斥组合计数计数dp思维

日期: 2026-06-20 07:35

题意

我们要从这些菜里选出一个方案。

因为同一种烹饪方法不能重复,所以每一行最多选一道菜;如果选了第 i 行的第 j 列,就有 a[i][j] 种不同的具体菜可选。

设最终一共选了 k 道菜,那么题目的限制是:每种食材出现次数都不能超过 floor(k/2)
换句话说,不能存在某种食材严格超过一半

需要统计所有合法方案数,答案对 998244353 取模。

思路

先看一个可以直接验证想法的朴素解:

cpp
#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 成为严格多数”的坏方案数

把所有食材的坏方案数加起来,再从总方案数里减掉,就是答案。

代码

cpp
#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;
}

复杂度

  • 枚举行和列读入:O(nm)O(nm)
  • 枚举每种食材做差值 DP:O(mn2)O(mn^2)
  • 空间复杂度:O(nm)O(nm)

总结

这题最关键的两步是:

  1. 把“每种食材都不能超过一半”改写成“不能存在严格多数食材”
  2. 固定一个候选多数食材后,把所有其它食材合并成一类,只维护两边数量差

一旦做到这一步,原本看起来像多维计数的问题,就被压成了一个很规整的一维 DP。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析