跳到主要内容

AcWing 886. 求组合数 II

本节目标

用阶乘和逆阶乘快速回答大范围组合数查询。

题意与约束

多组 a, b 的上界比 Pascal 表更大,需要输出 C(a,b) mod 1_000_000_007

第一反应与瓶颈

Pascal 三角需要 O(N²) 空间和时间,N=100000 时不可用。逐查询计算阶乘又浪费了共享前缀。

数学关系与算法推导

把每个全排列的前 b 个位置视作选中的元素。一个固定的 b 元子集会被重复计数 b!(a-b)! 次:选中部分有 b! 种内部顺序,未选部分有 (a-b)! 种内部顺序。因此 a!=C(a,b)·b!·(a-b)!,也就是 C(a,b)=a!/(b!(a-b)!)。模运算不能直接做这两个除法;因为本题模数是质数且相关阶乘非零,可预处理 fact[i]=i!,再用费马小定理求 invFact[N]=fact[N]^(MOD-2),从后向前递推 invFact[i-1]=invFact[i]·i。最终用 fact[a]·invFact[b]·invFact[a-b] 回答。

正确性依据

费马小定理保证非零阶乘在本题模数下有逆元。逆阶乘后推式恰好撤销一个因子,因此三个数组项的乘积等于阶乘公式中的分子乘分母逆元。

样例执行过程

C(100000,1)=100000,并且由对称性 C(100000,99999) 也为 100000,两个查询共享同一组预处理。

代码实现

C++17
#include <algorithm>
#include <iostream>
#include <utility>
#include <vector>
using namespace std;

constexpr long long MOD = 1000000007;

long long modPower(long long base, long long exponent) {
long long result = 1;
while (exponent > 0) {
if ((exponent & 1) != 0) {
result = result * base % MOD;
}
base = base * base % MOD;
exponent >>= 1;
}
return result;
}

vector<int> combinationQueriesFactorial(const vector<pair<int, int>>& queries) {
int maximum = 0;
for (const auto& [a, b] : queries) {
maximum = max(maximum, a);
}
vector<long long> factorial(maximum + 1, 1);
vector<long long> inverseFactorial(maximum + 1, 1);
for (int value = 1; value <= maximum; value++) {
factorial[value] = factorial[value - 1] * value % MOD;
}
if (maximum > 0) {
inverseFactorial[maximum] = modPower(factorial[maximum], MOD - 2);
for (int value = maximum; value > 0; value--) {
inverseFactorial[value - 1] = inverseFactorial[value] * value % MOD;
}
}

vector<int> answers;
answers.reserve(queries.size());
for (const auto& [a, b] : queries) {
if (b < 0 || b > a) {
answers.push_back(0);
} else {
answers.push_back(static_cast<int>(factorial[a] * inverseFactorial[b] % MOD * inverseFactorial[a - b] % MOD));
}
}
return answers;
}

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

int count;
cin >> count;
vector<pair<int, int>> queries(count);
for (auto& [a, b] : queries) {
cin >> a >> b;
}
for (int answer : combinationQueriesFactorial(queries)) {
cout << answer << '\n';
}
return 0;
}
#endif

复杂度分析

设最大 aN,预处理时间和空间为 O(N),每个查询 O(1);最后一次快速幂是 O(log MOD)

边界与易错点

C(0,0)=1。只有质数模数且阶乘不含该模数因子时,费马逆元才成立;乘法应在每一步取模,不能先做普通整数除法。

模式迁移

大范围、多查询、质数模数是“阶乘加逆元”模板的识别信号;上界更大或模数变化时不能机械套用。回到组合计数