高效计算组合数(二项式系数)

150 阅读2分钟

以下C++代码实现了一个用于高效计算组合数(二项式系数)的预处理器,它通过预计算阶乘和阶乘的逆元来加速组合数计算。以下是详细解析:


核心功能

  1. 预计算阶乘数组 fact[]
    • fact[i] = i! % MOD(i的阶乘对MOD取模)
    • 例如:fact[3] = 3! = 6
  2. 预计算阶乘逆元数组 inv_fact[]
    • inv_fact[i] = (i!)^(-1) % MOD(i的阶乘的模逆元)
    • 用于避免除法,将除法转化为乘法(数论基础)
  3. 支持快速计算组合数 组合数公式: (nk)=n!k!(nk)!modMOD\binom{n}{k} = \frac{n!}{k!(n-k)!} \mod \text{MOD} 可优化为: (nk)=fact[n]×inv_fact[k]×inv_fact[nk]modMOD\binom{n}{k} = \text{fact}[n] \times \text{inv\_fact}[k] \times \text{inv\_fact}[n-k] \mod \text{MOD}

代码逐行解析

const int MOD = 1e9 + 7;  // 模数(质数)
const int MX = 1e5;       // 预计算的最大范围(100,000)
long long fact[MX];       // 存储阶乘 % MOD
long long inv_fact[MX];   // 存储阶乘的逆元 % MOD

void init() {
    // 检查是否已初始化(避免重复计算)
    if (fact[0]) return;

    // 1. 计算阶乘数组 [0!, 1!, ..., (MX-1)!] % MOD
    fact[0] = 1;  // 0! = 1
    for (int i = 1; i < MX; i++) {
        fact[i] = fact[i - 1] * i % MOD;  // 递推计算阶乘
    }

    // 2. 计算阶乘逆元数组
    // 2.1 用费马小定理计算最后一个元素的逆元
    inv_fact[MX - 1] = qpow(fact[MX - 1], MOD - 2);  // 快速幂求逆元

    // 2.2 倒序递推计算所有逆元
    for (int i = MX - 1; i > 0; i--) {
        inv_fact[i - 1] = inv_fact[i] * i % MOD;  // 关键递推关系
    }
}

关键技巧详解

1. 逆元递推关系(核心优化)

  • 递推公式inv_fact[i1]=inv_fact[i]×imodMOD\text{inv\_fact}[i - 1] = \text{inv\_fact}[i] \times i \mod \text{MOD}
  • 数学推导: 由定义知: (i!)1inv_fact[i]modMOD(i!)^{-1} \equiv \text{inv\_fact}[i] \mod \text{MOD} 两边乘以ii得到 (i1)!(i-1)! 的逆元: (i!)1×i((i1)!)1modMOD(i!)^{-1} \times i \equiv ((i - 1)!)^{-1} \mod \text{MOD} (因为 i!=i×(i1)!i! = i \times (i-1)!

2. 倒序计算逆元的原因

  • 先计算最大的逆元 inv_fact[MX-1](通过快速幂)
  • 利用递推关系 反向计算 更小的逆元,避免对每个元素单独调用快速幂(时间复杂度从 O(nlogn)O(n \log n) 降至 O(n)O(n)

使用示例

计算组合数 (nk)\binom{n}{k} 的函数:

long long nCr(int n, int k) {
    if (k < 0 || k > n) return 0;
    init();  // 确保已初始化
    return fact[n] * inv_fact[k] % MOD * inv_fact[n - k] % MOD;
}

调用示例

cout << nCr(5, 2);  // 输出 C(5,2)=10 % MOD

复杂度分析

  • 时间复杂度O(n)O(n)(初始化仅需遍历两次数组)
  • 空间复杂度O(n)O(n)(存储两个长度为 MX 的数组)

应用场景

  1. 组合数学问题(如排列组合计数)
  2. 概率与统计(多项式系数计算)
  3. 动态规划优化(将组合数计算从 O(n)O(n) 降至 O(1)O(1)
  4. 算法竞赛(如Codeforces/LeetCode中需要快速计算大组合数的问题)