组合数:从基础理论到高效算法实现

一、组合数学基础

1.1 组合数定义
在离散数学中,组合数 C ( n , k ) C(n,k) C(n,k)(记为 ( n k ) \dbinom{n}{k} (kn))表示在 n n n 个不同元素中选取 k k k 个元素的非重复组合方式数量,其经典定义为:

( n k ) = n ! k ! ( n − k ) ! ( 0 ⩽ k ⩽ n ) \dbinom{n}{k} = \frac{n!}{k!(n-k)!} \quad (0 \leqslant k \leqslant n) (kn)=k!(nk)!n!(0kn)

1.2 递推关系
式如《算法导论》中常见的手法,我们从递归方程入手分析。组合数满足著名Pascal递推公式:

( n k ) = ( n − 1 k − 1 ) + ( n − 1 k ) \dbinom{n}{k} = \dbinom{n-1}{k-1} + \dbinom{n-1}{k} (kn)=(k1n1)+(kn1)

边界条件:
( n 0 ) = ( n n ) = 1 ∀ n ⩾ 0 \dbinom{n}{0} = \dbinom{n}{n} = 1 \quad \forall n \geqslant 0 (0n)=(nn)=1n0

二、经典算法实现

2.1 直接递归算法

int comb_recursive(int n, int k) {
    if (k == 0 || k == n) return 1;
    return comb_recursive(n-1, k-1) + comb_recursive(n-1, k);
}

复杂度分析:

  • 时间复杂度: O ( 2 n ) O(2^n) O(2n)(这显然无法接受)
  • 空间复杂度: O ( n ) O(n) O(n)(递归栈深度)

该算法适用于教学演示递归关系,但通过算法导论中的递归树分析可知存在大量重复计算,不具备实用价值。

2.2 动态规划解法

int comb_dp(int n, int k) {
    vector<vector<int>> dp(n+1, vector<int>(k+1, 0));
    for(int i = 0; i <= n; ++i) {
        for(int j = 0; j <= min(i, k); ++j) {
            dp[i][j] = (j == 0 || j == i) ? 1 : dp[i-1][j-1] + dp[i-1][j];
        }
    }
    return dp[n][k];
}

优化空间复杂度:

int comb_dp_optimized(int n, int k) {
    vector<int> dp(k+1, 0);
    dp[0] = 1;
    for(int i = 1; i <= n; ++i) {
        for(int j = min(i, k); j > 0; --j) {
            dp[j] += dp[j-1];
        }
    }
    return dp[k];
}

复杂度分析:

  • 时间复杂度: O ( n k ) O(nk) O(nk)
  • 空间复杂度:原算法 O ( n k ) O(nk) O(nk),优化后 O ( k ) O(k) O(k)

该方法通过递推存储中间结果避免了重复计算,达到多项式时间复杂度,适用于中小规模数据。

三、取模运算与高效算法

3.1 模运算预处理
当结果需要取模时( C ( n , k )   m o d   p C(n,k) \bmod p C(n,k)modp),我们结合数论中的Fermat小定理实现模逆运算:

const int MOD = 1e9+7;
vector<long long> fact, inv_fact;
 
void precompute(int n) {
    fact.resize(n+1);
    inv_fact.resize(n+1);
    fact[0] = 1;
    for(int i=1; i<=n; ++i) 
        fact[i] = fact[i-1] * i % MOD;
    
    inv_fact[n] = mod_pow(fact[n], MOD-2);
    for(int i=n-1; i>=0; --i)
        inv_fact[i] = inv_fact[i+1] * (i+1) % MOD;
}
 
int comb_mod(int n, int k) {
    return fact[n] * inv_fact[k] % MOD * inv_fact[n-k] % MOD;
}

复杂度分析:

  • 预处理时间复杂度: O ( n ) O(n) O(n)
  • 查询时间复杂度: O ( 1 ) O(1) O(1)

3.2 Lucas定理应用
对于大质数 p p p,当 n > p n > p n>p时需要采用分治策略:

int lucas_theorem(int n, int k, int p) {
    if(k == 0) return 1;
    return lucas_theorem(n/p, k/p, p) * comb_mod(n%p, k%p) % p;
}

该算法时间复杂度为 O ( log ⁡ p n ) O(\log_p n) O(logpn),主要应用于: p p p为质数且 n n n极大时的组合数取模计算。

四、算法选择策略

算法类型适用场景时间复杂度空间复杂度
Direct Recursive教学演示 O ( 2 n ) O(2^n) O(2n) O ( n ) O(n) O(n)
Dynamic Programming n ≤ 5000 n \leq 5000 n5000 O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n)
Modular Preprocess n ≤ 1 0 6 n \leq 10^6 n106 O ( n ) O(n) O(n) preprocessing O ( n ) O(n) O(n)
Lucas + Preprocess n ≤ 1 0 18 n \leq 10^{18} n1018, prime p ≤ 1 0 6 p \leq 10^6 p106 O ( p + log ⁡ p n ) O(p + \log_p n) O(p+logpn) O ( p ) O(p) O(p)

五、典型应用场景

5.1 二项式定理展开
( a + b ) n = ∑ k = 0 n ( n k ) a n − k b k (a+b)^n = \sum_{k=0}^{n} \dbinom{n}{k} a^{n-k}b^k (a+b)n=k=0n(kn)ankbk

5.2 组合数路径计数
m × n m \times n m×n网格图从左上到右下的路径数等于 ( m + n m ) \dbinom{m+n}{m} (mm+n)

int uniquePaths(int m, int n) {
    return comb_mod(m+n-2, min(m-1, n-1));
}

5.3 概率计算应用
在n次独立重复试验中的应用:
P ( k ) = ( n k ) p k ( 1 − p ) n − k P(k) = \dbinom{n}{k} p^k (1-p)^{n-k} P(k)=(kn)pk(1p)nk

六、进阶技巧

6.1 Catalan数计算
使用组合数表达式:
C n = 1 n + 1 ( 2 n n ) C_n = \frac{1}{n+1}\dbinom{2n}{n} Cn=n+11(n2n)

6.2 组合数奇偶性判定
利用Lucas定理可得:
( n k ) \dbinom{n}{k} (kn)为奇数的充要条件是 k k k的二进制表示是 n n n的子集


七、常用模板

#include <bits/stdc++.h>
using namespace std;

const int MOD = 1e9 + 7;    // 模数,根据题目要求修改为质数
const int N = 1e5 + 10;     // 预处理的最大范围,根据题目要求调整

long long fact[N];          // 存储阶乘 % MOD 的结果
long long inv_fact[N];      // 存储阶乘逆元 % MOD 的结果

long long qpow(long long a, long long b) {
    long long res = 1;
    while (b) {
        if (b & 1) res = res * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }
    return res;
}
void init_comb() {
    fact[0] = 1;
    // 计算阶乘
    for (int i = 1; i < N; ++i) {
        fact[i] = fact[i - 1] * i % MOD;
    }
    // 计算最大位置阶乘的逆元
    inv_fact[N - 1] = qpow(fact[N - 1], MOD - 2);
    // 递推计算阶乘逆元
    for (int i = N - 2; i >= 0; --i) {
        inv_fact[i] = inv_fact[i + 1] * (i + 1) % MOD;
    }
}


long long comb(int n, int k) {
    if (n < 0 || k < 0 || k > n) return 0;
    return fact[n] * inv_fact[k] % MOD * inv_fact[n - k] % MOD;
}

int main() {
    init_comb(); // 初始化,必须调用
    
    cout << "C(5, 2) = " << comb(5, 2) << endl; // 输出 10
    cout << "C(100000, 50000) = " << comb(100000, 50000) << endl; // 需要保证 N > 100000
    
    return 0;
}
输出样例:
C(5, 2) = 10
C(100000, 50000) = 149033233

总结:

组合数的计算既是经典的数学问题,也蕴含着丰富的算法思想。在算法竞赛中也常常出现,要好好理解组合数的思想,感受下组合数的魅力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值