const int MAXN = 1000010;
const ll MOD = 1000000007;
ll fac[MAXN], ifac[MAXN];
ll qpow(ll b, ll e) {
ll r = 1 % MOD; b %= MOD;
while (e) { if (e & 1) r = r * b % MOD; b = b * b % MOD; e >>= 1; }
return r;
}
void init() {
fac[0] = 1;
for (int i = 1; i < MAXN; i++) fac[i] = fac[i - 1] * i % MOD;
ifac[MAXN - 1] = qpow(fac[MAXN - 1], MOD - 2);
for (int i = MAXN - 2; i >= 0; i--) ifac[i] = ifac[i + 1] * (i + 1) % MOD;
}
// 求 n! / (a[0]! * a[1]! * ... ) mod MOD
ll multisetPerm(ll n, const vector<ll> &a) {
ll res = fac[n];
for (ll ai : a) res = res * ifac[ai] % MOD;
return res;
}
3多重集的组合:隔板法 高频
问题:从 k 类元素中一共取出 n 个(每类可以取任意多个,允许取 0 个),有多少种不同的取法?"不同"指的是各类取出的数量组合不同,与顺序无关。
#include<bits/stdc++.h>using namespace std;
typedef long long ll;
const int MAXN = 1000010;
const ll MOD = 1000000007;
ll fac[MAXN], ifac[MAXN];
ll qpow(ll b, ll e) {
ll r = 1 % MOD; b %= MOD;
while (e) { if (e & 1) r = r * b % MOD; b = b * b % MOD; e >>= 1; }
return r;
}
void init() {
fac[0] = 1;
for (int i = 1; i < MAXN; i++) fac[i] = fac[i - 1] * i % MOD;
ifac[MAXN - 1] = qpow(fac[MAXN - 1], MOD - 2); // 费马小定理求一次逆元for (int i = MAXN - 2; i >= 0; i--) ifac[i] = ifac[i + 1] * (i + 1) % MOD;
}
int main() {
init();
int n, k; cin >> n >> k;
vector<ll> a(k);
for (int i = 0; i < k; i++) cin >> a[i];
ll ans = fac[n];
for (int i = 0; i < k; i++) ans = ans * ifac[a[i]] % MOD;
cout << ans << endl;
return0;
}
#include<bits/stdc++.h>using namespace std;
typedef long long ll;
const ll MOD = 1000000007;
ll qpow(ll b, ll e) {
ll r = 1 % MOD; b %= MOD;
while (e) { if (e & 1) r = r * b % MOD; b = b * b % MOD; e >>= 1; }
return r;
}
int main() {
ll n; int t; cin >> n >> t;
// 先算 (n-1)! mod MOD
ll ans = 1 % MOD;
for (ll i = 1; i <= n - 1; i++) ans = ans * (i % MOD) % MOD;
if (t == 2) ans = ans * qpow(2, MOD - 2) % MOD; // 项链:再除以 2
cout << ans << endl;
return0;
}
易错:隔板法公式里上下标颠倒(写成 C(n+k−1, k));"至少 1 个"忘记先判 n < k;容斥的符号交替写错;阶乘表开得太小导致越界。
题2综合编程 · 错排
输入 T 组 (n, m),每组输出 1..n 的排列中"至少有 m 个元素在原位"的排列个数,对 10⁹+7 取模。
数据范围:T ≤ 10⁵,1 ≤ n ≤ 10⁶。
查看参考答案
思路:"至少 m 个"= 恰好 m 个 + 恰好 m+1 个 + … + 恰好 n 个。而"恰好 k 个"= C(n,k)·D_{n−k}。所以:
答案 = Σ_{k=m}^{n} C(n, k) · D_{n−k}
关键观察:直接形式共有 n − m + 1 项。而利用恒等式
Σ_{k=0}^{n} C(n, k) · D_{n−k} = n!
(因为每个排列按"不动点个数"恰好被分到唯一一个 k 类里,全部加起来就是排列总数),同一个答案还能写成"取补集"的形式:
答案 = n! − Σ_{k=0}^{m−1} C(n, k) · D_{n−k}
于是有两条路:完整求和 Σ_{k=m}^{n} 要循环 n − m + 1 次;取补集只要循环 m 次。两种写法的结果是同一个数,谁的项数少就用谁——单次询问复杂度为 O(min(m, n − m + 1))。这一步不是"优化技巧",而是恒等变形,结果严格相等。
复杂度与数据范围:预处理阶乘、阶乘逆元、错排表共 O(N)(N = max n);每组询问 O(min(m, n − m + 1))。因此把范围补上 Σ min(mᵢ, nᵢ − mᵢ + 1) ≤ 10⁷("至少 m 个 / 至多 m 个"类问题的常见约定),程序就严格可过。下面按"取项数更少者"实现:
#include<bits/stdc++.h>using namespace std;
typedef long long ll;
const int MAXN = 1000010;
const ll MOD = 1000000007;
ll fac[MAXN], ifac[MAXN], D[MAXN];
ll qpow(ll b, ll e) {
ll r = 1 % MOD; b %= MOD;
while (e) { if (e & 1) r = r * b % MOD; b = b * b % MOD; e >>= 1; }
return r;
}
ll C(ll n, ll m) {
if (m < 0 || m > n) return0;
return fac[n] * ifac[m] % MOD * ifac[n - m] % MOD;
}
int main() {
ios::sync_with_stdio(false); cin.tie(0);
fac[0] = 1;
for (int i = 1; i < MAXN; i++) fac[i] = fac[i - 1] * i % MOD;
ifac[MAXN - 1] = qpow(fac[MAXN - 1], MOD - 2);
for (int i = MAXN - 2; i >= 0; i--) ifac[i] = ifac[i + 1] * (i + 1) % MOD;
D[0] = 1; D[1] = 0;
for (int i = 2; i < MAXN; i++) D[i] = (i - 1) * ((D[i - 1] + D[i - 2]) % MOD) % MOD;
int T; cin >> T;
while (T--) {
ll n, m; cin >> n >> m;
if (m > n) { cout << 0 << endl; continue; }
ll ans;
if (m <= n - m + 1) {
// 补集形式:n! − Σ_{k=0}^{m−1} C(n,k)·D_{n−k},共 m 项
ans = fac[n];
for (ll k = 0; k < m; k++) ans = (ans - C(n, k) * D[n - k] % MOD + MOD) % MOD;
} else {
// 直接形式:Σ_{k=m}^{n} C(n,k)·D_{n−k},共 n−m+1 项
ans = 0;
for (ll k = m; k <= n; k++) ans = (ans + C(n, k) * D[n - k]) % MOD;
}
cout << ans << endl;
}
return0;
}
特判 m = 1 的情形:如果所有人都被捆成一个块(即所有 n 人首尾相接成一条链),那"围成一圈"只有 1 种本质不同的坐法(链首尾相接),答案就是 1(而不是 0! = 1 再乘 2 —— 因为此时链的两端也相邻,翻转得到的圆排列是同一个)。
#include<bits/stdc++.h>using namespace std;
typedef long long ll;
const ll MOD = 1000000007;
const int MAXN = 100010;
int fa[MAXN], sz[MAXN], deg[MAXN];
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
ll qpow(ll b, ll e) {
ll r = 1 % MOD; b %= MOD;
while (e) { if (e & 1) r = r * b % MOD; b = b * b % MOD; e >>= 1; }
return r;
}
int main() {
ios::sync_with_stdio(false); cin.tie(0);
int n, q; cin >> n >> q;
for (int i = 1; i <= n; i++) fa[i] = i, sz[i] = 1;
for (int t = 0; t < q; t++) {
int u, v; cin >> u >> v;
deg[u]++; deg[v]++;
if (deg[u] > 2 || deg[v] > 2) { cout << "-1" << endl; return0; } // 出现分叉int ru = find(u), rv = find(v);
if (ru == rv) { cout << "-1" << endl; return0; } // 出现环
fa[ru] = rv; sz[rv] += sz[ru];
}
// 统计块数,并累乘每块的翻转系数
ll m = 0, coef = 1;
for (int i = 1; i <= n; i++) {
if (find(i) != i) continue;
m++;
if (sz[i] >= 2) coef = coef * 2 % MOD;
}
if (m == 1) { cout << 1 << endl; return0; } // 全体成一条链,围成圈只有 1 种// (m-1)! 部分
ll ans = coef;
for (ll i = 1; i <= m - 1; i++) ans = ans * (i % MOD) % MOD;
cout << ans << endl;
return0;
}
先看"固定一个人"为什么有效。圆排列的对称操作是 n 个旋转,它们构成一个大小为 n 的循环群。想数"轨道数",等价于数"在旋转下的代表元个数"。因为每个轨道恰好有 n 个元素、且没有排列能被非平凡旋转固定(珠子可分辨时),所以"固定 1 号在正上方"就唯一选出了每个轨道的一个代表,剩下 n − 1 个人可以任意排 → (n − 1)!。
#include<bits/stdc++.h>using namespace std;
typedef long long ll;
const int MAXN = 1000010;
const ll MOD = 1000000007;
ll fac[MAXN], ifac[MAXN];
ll qpow(ll b, ll e) {
ll r = 1 % MOD; b %= MOD;
while (e) { if (e & 1) r = r * b % MOD; b = b * b % MOD; e >>= 1; }
return r;
}
int main() {
ios::sync_with_stdio(false); cin.tie(0);
string s; cin >> s;
int n = s.size();
fac[0] = 1;
for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;
ifac[n] = qpow(fac[n], MOD - 2);
for (int i = n - 1; i >= 0; i--) ifac[i] = ifac[i + 1] * (i + 1) % MOD;
vector<int> cnt(26, 0);
for (char c : s) cnt[c - 'a']++;
ll ans = fac[n];
for (int i = 0; i < 26; i++) ans = ans * ifac[cnt[i]] % MOD;
cout << ans << endl;
return0;
}
样例:aabb → 6;abc → 6;aaaa → 1;mississippi → 34650。
易错:阶乘表只开到字符串长度不够用(要在读入后按 n 计算);求逆元时对 0! 也求一次(用 ifac[n] 起点往下递推即可);count 数组下标用 c - 'A' 而出错。
B2综合编程
预处理 D₀ ~ D₁₀⁶,输入 T 组 (n, k),输出 1..n 的排列中恰好有 k 个元素在原位的方案数,对 10⁹+7 取模。
查看参考答案
思路:答案 = C(n, k) × D[n−k]。预处理阶乘、阶乘逆元、错排表,每组 O(1)。
#include<bits/stdc++.h>using namespace std;
typedef long long ll;
const int MAXN = 1000010;
const ll MOD = 1000000007;
ll fac[MAXN], ifac[MAXN], D[MAXN];
ll qpow(ll b, ll e) {
ll r = 1 % MOD; b %= MOD;
while (e) { if (e & 1) r = r * b % MOD; b = b * b % MOD; e >>= 1; }
return r;
}
ll C(ll n, ll m) {
if (m < 0 || m > n) return0;
return fac[n] * ifac[m] % MOD * ifac[n - m] % MOD;
}
int main() {
ios::sync_with_stdio(false); cin.tie(0);
fac[0] = 1;
for (int i = 1; i < MAXN; i++) fac[i] = fac[i - 1] * i % MOD;
ifac[MAXN - 1] = qpow(fac[MAXN - 1], MOD - 2);
for (int i = MAXN - 2; i >= 0; i--) ifac[i] = ifac[i + 1] * (i + 1) % MOD;
D[0] = 1; D[1] = 0;
for (int i = 2; i < MAXN; i++) D[i] = (i - 1) * ((D[i - 1] + D[i - 2]) % MOD) % MOD;
int T; cin >> T;
while (T--) {
ll n, k; cin >> n >> k;
cout << C(n, k) * D[n - k] % MOD << endl;
}
return0;
}
输入 n 与 k(k ≤ n ≤ 10⁶),输出"n 个人围 n 个座位的圆桌、固定 k 个座位无人"的方案数,对 10⁹+7 取模。说明这与圆排列公式的关系。
查看参考答案
题意澄清:座位是固定编号的(1..n 号位,围成一圈),要选 k 个座位空着,剩下 n − k 个人坐进去。求方案数。
思路:分两步 —— ① 选哪 k 个座位空着:C(n, k);② 把 n − k 个人排到剩下的 n − k 个有编号座位上:(n − k)!。
答案 = C(n, k) · (n − k)! = n! / k!
#include<bits/stdc++.h>using namespace std;
typedef long long ll;
const int MAXN = 1000010;
const ll MOD = 1000000007;
ll fac[MAXN];
ll qpow(ll b, ll e) {
ll r = 1 % MOD; b %= MOD;
while (e) { if (e & 1) r = r * b % MOD; b = b * b % MOD; e >>= 1; }
return r;
}
int main() {
ll n, k; cin >> n >> k;
fac[0] = 1;
for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;
// C(n,k) * (n-k)! = n!/(k!(n-k)!) * (n-k)! = n!/k!
cout << fac[n] * qpow(fac[k], MOD - 2) % MOD << endl;
return0;
}
【插空法】用 a 个 A、b 个 B、c 个 C 组成长度 a + b + c 的串,要求任意两个 A 都不相邻。求方案数对 10⁹+7 取模。
查看参考答案
思路:先排 B 和 C(共 b + c 个元素),它们之间有 b + c + 1 个空隙;把 a 个 A 放进这些空隙里,每个空隙至多放 1 个(保证 A 不相邻)。所以:
答案 = [ (b + c)! / (b! · c!) ] × C(b + c + 1, a)
第一个因子是 B、C 自身的多重集排列数;第二个因子是"选 a 个空隙放 A"。
#include<bits/stdc++.h>using namespace std;
typedef long long ll;
const int MAXN = 1000010;
const ll MOD = 1000000007;
ll fac[MAXN], ifac[MAXN];
ll qpow(ll b, ll e) {
ll r = 1 % MOD; b %= MOD;
while (e) { if (e & 1) r = r * b % MOD; b = b * b % MOD; e >>= 1; }
return r;
}
ll C(ll n, ll m) {
if (m < 0 || m > n || n < 0) return0;
return fac[n] * ifac[m] % MOD * ifac[n - m] % MOD;
}
int main() {
ll a, b, c; cin >> a >> b >> c;
int N = a + b + c + 1;
fac[0] = 1;
for (int i = 1; i <= N; i++) fac[i] = fac[i - 1] * i % MOD;
ifac[N] = qpow(fac[N], MOD - 2);
for (int i = N - 1; i >= 0; i--) ifac[i] = ifac[i + 1] * (i + 1) % MOD;
ll bc = fac[b + c] * ifac[b] % MOD * ifac[c] % MOD; // B、C 的排列数
ll gaps = C(b + c + 1, a); // 选 a 个空隙放 A
cout << bc * gaps % MOD << endl;
return0;
}