数有多少个面试排列能让录用人数至少为 m。
P14364 [CSP-S 2025] 员工招聘
贡献延后 DP:未录用人数每增加 1,就结算一批刚刚失去机会的人。
一、先回答问题
把“放弃参加面试”当作什么都没发生;实际上他也会让未录用人数加 1。
二、核心观察
建模状态记录未录用人数 j,以及还没具体定身份的高耐心人数 k。
优化把 c>j 的人暂时延后统计,等 j 增加时再用组合数结算 c=j+1 的人。
正确性能否参加只由当前未录用人数 j 和 c_i 比较决定;j 单调增加,所以每个耐心层只会被结算一次。
三、互动演示
--
--
--
1:准备开始。
四、自己选答案
为什么可以按 c 的层数延后统计?
先自己判断,再点选项。
最后应该累加哪些 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` 的人一开始就可能放弃,不能漏。