先写出原题的 O(nk) 计数 DP,再把状态改写成第二类 Stirling 数,最后用满射计数的容斥公式在线性预处理后求出 S(n,n-k)。
OJ: luogu
题目 ID: P6162
难度:提高+/省选-
标签:组合计数容斥数学推导动态规划
日期: 2026-06-20 08:03
题意
有 n-1 个格子,第 i 个格子可以不填,或者填一个 1..i 之间的正整数。
要求:
- 恰好有
k个格子被填 - 所有填入的数字两两不同
求合法方案数,答案对 998244353 取模。
思路
先看一个可以直接验证想法的朴素解:
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const i64 MOD = 998244353LL;
int n, k;
vector<i64> dp;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
dp.assign(k + 1, 0);
dp[0] = 1;
// 直接按题意做 DP:
// dp[j] = 处理完前若干个格子,恰好填了 j 个格子的方案数
//
// 处理第 i 个格子时:
// 1. 不填,方案数不变
// 2. 填一个数。若之前已经填了 j-1 个互不相同的数,
// 那么 1..i 中还剩 i-(j-1) = i-j+1 个数可选。
for (int i = 1; i <= n - 1; i++) {
for (int j = min(i, k); j >= 1; j--) {
dp[j] = (dp[j] + 1LL * (i - j + 1) * dp[j - 1]) % MOD;
}
}
cout << dp[k] << '\n';
return 0;
}这个 brute.cpp 其实已经不是 DFS 暴力了,而是按题意直接写出的
设 f[i][j] 表示处理完前 i 个格子,恰好填了 j 个格子的方案数。
处理第 i 个格子时:
- 不填:贡献
f[i-1][j] - 填:若前面已经填了
j-1个不同数字,那么1..i中还剩i-j+1个可选新数字
所以递推是:
f[i][j] = f[i-1][j] + (i-j+1) * f[i-1][j-1]
一个小表格
这张表展示 n=5 时,前几层 f[i][j] 的变化:
i \\ j |
0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 0 | 1 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 0 | 0 |
| 2 | 1 | 3 | 1 | 0 | 0 |
| 3 | 1 | 6 | 7 | 1 | 0 |
| 4 | 1 | 10 | 25 | 15 | 1 |
如果你熟悉组合数学,会发现这张表非常像第二类 Stirling 数。
令:
g[i][m] = f[i][i+1-m]
代入后得到:
g[i][m] = g[i-1][m-1] + m * g[i-1][m]
这正是第二类 Stirling 数 S(i+1,m) 的标准递推,因此:
f[n-1][k] = S(n,n-k)
接下来问题就变成如何快速求一个第二类 Stirling 数单点值。
用“满射计数”可以得到容斥公式:
S(n,m) = 1 / m! * sum_{i=0}^{m} (-1)^i C(m,i)(m-i)^n
其中 m = n-k。
于是我们只要:
- 预处理
0..m的阶乘和逆阶乘 - 枚举容斥项
i - 计算
(m-i)^n / (i!(m-i)!) - 按奇偶加减
就能在线性复杂度内求出答案。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int MAXN = 1000000 + 5;
const i64 MOD = 998244353LL;
int n, k;
int group_cnt;
i64 fact[MAXN], inv_fact[MAXN];
i64 quick_pow(i64 base, int exp) {
i64 ans = 1;
base %= MOD;
while (exp > 0) {
if (exp & 1) {
ans = ans * base % MOD;
}
base = base * base % MOD;
exp >>= 1;
}
return ans;
}
void init_comb(int up) {
fact[0] = 1;
for (int i = 1; i <= up; i++) {
fact[i] = fact[i - 1] * i % MOD;
}
inv_fact[up] = quick_pow(fact[up], (int)MOD - 2);
for (int i = up; i >= 1; i--) {
inv_fact[i - 1] = inv_fact[i] * i % MOD;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
group_cnt = n - k;
init_comb(group_cnt);
// 原题答案 = 第二类 Stirling 数 S(n, n-k)
// 用满射计数的容斥公式:
// S(n,m) = 1 / m! * sum_{i=0}^{m} (-1)^i C(m,i) (m-i)^n
// = sum_{i=0}^{m} (-1)^i (m-i)^n / (i! (m-i)!)
i64 ans = 0;
for (int i = 0; i <= group_cnt; i++) {
int remain = group_cnt - i;
i64 term = quick_pow(remain, n);
term = term * inv_fact[i] % MOD * inv_fact[remain] % MOD;
if (i & 1) {
ans -= term;
if (ans < 0) {
ans += MOD;
}
}
else {
ans += term;
if (ans >= MOD) {
ans -= MOD;
}
}
}
cout << ans << '\n';
return 0;
}复杂度
设 m = n-k。
- 预处理阶乘、逆阶乘:
- 容斥枚举
m+1项,每项一次快速幂: - 空间复杂度:
总结
这题最重要的不是直接记住第二类 Stirling 数,而是先从原题按题意写出:
f[i][j] = f[i-1][j] + (i-j+1)f[i-1][j-1]
然后再通过状态代换识别出它其实就是 S(n,n-k)。
一旦识别成功,后面的正式做法就是标准的“第二类 Stirling 数 + 满射计数容斥”。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

