P14364 [CSP-S 2025] 员工招聘

贡献延后 DP:未录用人数每增加 1,就结算一批刚刚失去机会的人。

课堂版 先建模 再看代码

一、先回答问题

问题本质

数有多少个面试排列能让录用人数至少为 m。

容易走偏

把“放弃参加面试”当作什么都没发生;实际上他也会让未录用人数加 1。

二、核心观察

把题目变成模型
建模状态记录未录用人数 j,以及还没具体定身份的高耐心人数 k。
优化把 c>j 的人暂时延后统计,等 j 增加时再用组合数结算 c=j+1 的人。
正确性能否参加只由当前未录用人数 j 和 c_i 比较决定;j 单调增加,所以每个耐心层只会被结算一次。

三、互动演示

--
--
--

四、自己选答案

问题 1

为什么可以按 c 的层数延后统计?

问题 2

最后应该累加哪些 bad?

五、手推结果

情况j 是否增加需要做什么
当天 s=1 且 c>j不增加录用,k 加 1
当天 s=1 且 c<=j增加选一个低耐心人,并结算 c=j+1
当天 s=0 且 c>j+1增加当天人延后,k 调整
当天 s=0 且 c<=j+1增加当天人具体计入低耐心集合

六、C++14 参考代码

可提交代码
#include <bits/stdc++.h>
using namespace std;

const int N = 505;
const int MOD = 998244353;

int addMod(int a, long long b) {
    a = (a + b) % MOD;
    return a;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m;
    cin >> n >> m;

    string str;
    cin >> str;

    vector<int> s(n + 1), cnt(n + 2, 0), pre(n + 2, 0);
    for (int i = 1; i <= n; i++) s[i] = str[i - 1] - '0';

    for (int i = 1; i <= n; i++) {
        int c;
        cin >> c;
        cnt[c]++;
    }

    pre[0] = cnt[0];
    for (int i = 1; i <= n + 1; i++) {
        pre[i] = pre[i - 1] + cnt[i];
    }

    static int C[N][N], fac[N];
    fac[0] = 1;
    for (int i = 0; i < N; i++) C[i][0] = 1;
    for (int i = 1; i < N; i++) {
        fac[i] = 1LL * fac[i - 1] * i % MOD;
        for (int j = 1; j <= i; j++) {
            C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
            if (C[i][j] >= MOD) C[i][j] -= MOD;
        }
    }

    static int dp[2][N][N];
    dp[0][0][0] = 1;

    for (int i = 0; i < n; i++) {
        int cur = i & 1;
        int nxt = cur ^ 1;

        for (int j = 0; j <= n; j++) {
            for (int k = 0; k <= n; k++) dp[nxt][j][k] = 0;
        }

        for (int j = 0; j <= i; j++) {
            for (int k = 0; k <= i; k++) {
                int val = dp[cur][j][k];
                if (!val) continue;

                if (s[i + 1] == 1) {
                    // 选择一个 c > j 的人,他会被录用,身份延后统计。
                    if (n - pre[j] - k > 0) {
                        dp[nxt][j][k + 1] = addMod(dp[nxt][j][k + 1], val);
                    }

                    // 选择一个 c <= j 的人,他会放弃,未录用人数增加。
                    int lowAvailable = pre[j] - (i - k);
                    if (lowAvailable > 0) {
                        int lim = min(cnt[j + 1], k);
                        for (int t = 0; t <= lim; t++) {
                            long long ways = val;
                            ways = ways * fac[t] % MOD * C[k][t] % MOD;
                            ways = ways * C[cnt[j + 1]][t] % MOD;
                            ways = ways * lowAvailable % MOD;
                            dp[nxt][j + 1][k - t] =
                                addMod(dp[nxt][j + 1][k - t], ways);
                        }
                    }
                } else {
                    // 当天题难,无论选谁,都新增一个未录用者。
                    int lim = min(cnt[j + 1], k);
                    for (int t = 0; t <= lim; t++) {
                        long long base = val;
                        base = base * fac[t] % MOD * C[k][t] % MOD;
                        base = base * C[cnt[j + 1]][t] % MOD;

                        if (n - pre[j + 1] - (k - t) > 0) {
                            dp[nxt][j + 1][k - t + 1] =
                                addMod(dp[nxt][j + 1][k - t + 1], base);
                        }

                        int lowAvailable = pre[j + 1] - (i - (k - t));
                        if (lowAvailable > 0) {
                            long long ways = base * lowAvailable % MOD;
                            dp[nxt][j + 1][k - t] =
                                addMod(dp[nxt][j + 1][k - t], ways);
                        }
                    }
                }
            }
        }
    }

    int cur = n & 1;
    long long ans = 0;
    for (int bad = 0; bad <= n - m; bad++) {
        int k = n - pre[bad];
        ans = (ans + 1LL * dp[cur][bad][k] * fac[k]) % MOD;
    }

    cout << ans << '\n';
    return 0;
}
滚动数组把空间降到 O(n^2)。
`C(k,t)*C(cnt[j+1],t)*t!` 用来结算延后身份。
`c=0` 的人一开始就可能放弃,不能漏。