求组合数

发布时间 2023-04-07 21:13:09作者: 2huk

组合数

前置

组合数一般记为 \(C_a^b\),表示从 \(a\) 个不同的物品中选择 \(b\) 个物品的方案数,且不计顺序。

例如 ,\(C_5^3\) 表示从 \(5\) 个物品中选择 \(3\) 个物品。假设 \(5\) 个物品分别编号为 \(1,2,3,4,5\),那么可以进行如下选择:

  • \(1,2,3\)
  • \(1,2,4\)
  • \(1,2,5\)
  • \(1,3,4\)
  • \(1,3,5\)
  • \(1,4,5\)
  • \(2,3,4\)
  • \(2,3,5\)
  • \(2,4,5\)
  • \(3,4,5\)

\(10\) 种方案,也就是说 \(C_5^3 = 10\)

1. 递推 \(\Theta(n^2)\)

数据范围 : \(T\le 10^4,b\le a\le 2 \times 10^3\)

因为 \(C_a^b\) 表示从 \(a\) 个不同的物品中选择 \(b\) 个物品的方案数,那么我们将所有的选法分成两种情况。

首先随机挑出 \(1\) 个物品。

第一种情况,选完后包含这个物品,那么就需要再剩下的 \(a-1\) 个物品之中再选 \(b-1\) 个物品,即 \(C_{a-1}^{b-1}\)

第二种情况,选完后不包含这个物品,那么就需要在剩下的 \(a-1\) 个物品中选 \(a\) 个物品,即 \(C_{a-1}^b\)

因此可以得到递推式 \(C_a^b = C_{a-1}^{b-1} + C_{a-1}^b\)

void init(){
    for(int i=0; i<N; i++){
        for(int j=0; j<=i; j++){
            if(!j){
                C[i][j] = 1;
            }
            else{
                C[i][j] = add(C[i-1][j], C[i-1][j-1]);
            }
        }
    }
}

2. 公式 \(\Theta(n \cdot \log_{2}{n})\)

数据范围 : \(T \le 10^ 4,b\le a \le 10^5\)

\(C_n^m\) 的公式为 \(\dfrac{a!}{(a-b!) \cdot b!}\),可以预处理出所有数的阶乘值 \(fact_i\) ,再利用公式解答。

其中算除以一个数的阶乘要变为乘它阶乘的逆元 \(infact_i\),表示 \(i!\) 的逆元,也需要预处理。

因此,\(C_a^b = fact_a \times infact_{a-b} \times infact_b\)

int fact[N];    //   fact[i] 表示 i  的阶乘
int infact[N];  // infact[i] 表示 i! 的逆元

int fpm(int a, int b){	// 快速幂 
    int res = 1;
    while(b){
        if(b&1){
            res = mul(res, a);
        }
        b >>= 1;
        a = mul(a, a);
    }
    return res;
}

void init(){
    fact[0] = infact[0] = 1;
    
    for(int i=1; i<N; i++){
        fact[i] = mul(fact[i-1] * i);	// 求阶乘 
        infact[i] = mul(infact[i-1], fpm(i, mod - 2));	// 求逆元 
    }
}

int C(int a, int b){
	return mul(fact[a], infact[b], infact[a-b]);
}

3. 卢卡斯定理 \(\Theta(\log_{p}{n} \cdot p \cdot \log_{2}{p})\)

数据范围 : \(T \le 20,b \le a \le 10^{18},p \le 10^5,p是质数\)

转化

首先将 \(a\)\(b\) 化成 \(p\) 进制,即

\(a = a_k \cdot p^k + a_{k-1} \cdot p^{k-1} + \dots + a_0 \cdot p^0\)

\(b = b_k \cdot p^k + b_{k-1} \cdot p^{k-1} + \dots + b_0 \cdot p^0\)

生成函数

\((1 + x)^p = C_p^0 \cdot x^0 + C_p^1 \cdot x^1 + C_p^2 \cdot x^2 + \dots + C_p^p \cdot x^p\)

因为 \(p\) 是质数,所以 \(p\) 不包含小于 \(p\) 的质因子,所以这些项都能整除 \(p\)
所以,将中间的项消掉,就得到 \((1 + x)^p \equiv 1 + x^p \pmod p\)

展开

\(a\) 带入到 \((1 + x)^p\) 中的 \(p\),就可以得到

\[\left(1+x\right)^a \equiv \left(1+x\right)^{a_0} \cdot \left[\left(1+x\right)^p\right]^{a_1} \cdot \left[\left(1+x\right)^{p^2}\right]^{a_2} \cdot \left[\left(1+x\right)^{p^3}\right]^{a_3} \dots \cdot \left[\left(1+x\right)^{p^k}\right]^{a_k} \pmod p \]

把内部的括号去掉,还可以得到

\[\left(1+x\right)^a \equiv \left(1+x\right)^{a_0} \cdot \left(1+x^p\right)^{a_1} \cdot \left(1+x^{p^2}\right)^{a_2} \cdot \left(1+x^{p^3}\right)^{a_3} \dots \cdot \left(1+x^{p^k}\right)^{a_k} \pmod p \]

结论

\(C_a^b \equiv C_{a_k}^{b_k} \cdot C_{a_{k-1}}^{b_{k-1}} \dots C_{a_0}^{b_0} \pmod p\)

\(C_a^b \equiv C_{a\mod p}^{b\mod p} \cdot C_{a/p}^{b/p} \pmod{p}\)

int C(int a, int b){
    if(b > a){
        return 0;
    }
    
    int res = 1;
    for(int i=1, j=a; i<=b; i++, j--){
        // 相当于 res = res * j / i
        res = mul(res, j);
        res = mul(res, fpm(i, p-2));
    }
    
    return res;
}

int lucas(int a, int b){
    if(a < p && b < p){
        return C(a, b);
    }
    return mul(C(a%p, b%p), lucas(a/p, b/p));
}