欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

[数论]寻找组合数的四种方法

最编程 2024-04-08 18:53:10
...

组合数的常用公式

1.\; \; \; C_n^m = \frac{A_n^m}{A_m^m}

2.\; \; \; C_n^m = \frac{n!}{m!(n - m)!}

3.\; \; \; C_{n + 1}^m = C_n^m + C_n^{m - 1}

零、纯暴力法

根据第一个公式,将C_n^m的分子和分母求出,再相除即可。

适用范围:n,m较小的情况下。

时间复杂度O(m)

第一种部分代码如下:

for(int i = n; i >= n - m + 1; i--)
    ans *= i;
for(int i = m; i >= 1; i--)
    ans /= i;

第二种部分代码如下:

fz = 1, fm = 1;    // fz表示分子,fm表示分母
for(int i = n, j = 1; j <= m; i--, j++){
    fz *= i;
    fm *= j;
}
ans = fz / fm;

一、杨辉三角法

上面的第三个公式是符合杨辉三角的公式,因此可以使用第三个公式进行预处理题目范围内所有的C_n^m,需要时直接使用即可。

适用范围:n,m大小在10^3左右。

时间复杂度O(n^2)

C++部分代码如下:

// c[n][m]表示从n个元素中取m个的方案数
for(int i= 0; i <= N; i++)
    for(int j = 0; j <= i; j++)
        if(!j) c[i][j] = 1;
        else c[i][j] = c[i - 1][j] + c[i - 1][j - 1];

二、预处理法

先预处理出范围内所有数的阶乘和阶乘逆元,根据第一个公式直接取需要的阶乘和阶乘逆元的值即可。

适用范围:n,m在10^5以内,且取模的数mod为素数时。

时间复杂度O(N \: log \: N)

乘法逆元的求法可以参照本人之前的博客:求逆元的四种方法_Wmiracle的博客-****博客

由于这里取模的数为素数,因此可以用费马小定理求逆元。

C++部分代码如下:

ll quick_pow(ll a, ll b){    // 快速幂
	ll res = 1;
	while(b){
		if(b & 1) res = res * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return res;
}

void init(){    // 预处理,fac[]表示阶乘, inf[]表示阶乘的逆元
	fac[0] = inf[0] = 1;
	for(int i = 1; i <= N; i++){
		fac[i] = fac[i - 1] * i % mod;
		inf[i] = inf[i - 1] * quick_pow(i, mod - 2) % mod;
	}
}

// ans = fac[n] * inf[m] % mod * inf[n - m] % mod;

三、Lucas定理

Lucas定理简单来说是这样的:如果p是素数,对于任意整数1 \leqslant m \leqslant n,都有C_n^m \equiv C_{n % p}^{m % p} * C_{n / p}^{m / p} \; \; (mod \; p),其中C_{n / p}^{m / p}可以再进行分解,直至n,m小于p为止。

更为标准的Lucas定理式为:

C_n^m = \prod_{i = 1}^{k}C_{n_i}^{m_i} \; \; (mod \; p)

其中n = n_kp^k + n_{k - 1}p^{k - 1} + ... + n_1p + n_0

       m = m_kp^k + m_{k - 1}p^{k - 1} + ... + m_1p + m_0

适用范围:n,m较大但不超过10^{18}(当然正常也不会超过),且p的数量级不超过10^5时。

时间复杂度O(p \: log \: n)

证明过程:因为p为素数,当1 \leqslant j \leqslant p - 1时,都有C_p^j = \frac{p}{j}C_{p - 1}^{j - 1} \equiv 0 \; \; (mod \; p)

                  于是就有(1 + x)^n = (1 + x)^{n_0} * ((1 + x)^p)^{n_1} * ... * ((1 + x)^{p^k})^{n_k}

                                                \equiv (1 + x)^{n_0} * (1 + x^p)^{n_1} * ... * (1 + x^{p^k})^{n_k} \; \; (mod \; p)

                  对比两边x^m项的系数,结合二项式定理,C_n^m \equiv C_{n_k}^{m_k} * C_{n_{k - 1}}^{m_{k - 1}} * ... * C_{n_0}^{m_0} \; \; (mod \; p)                      得证。

C++部分代码如下:

ll quick_pow(ll a, ll b, int p){    // 快速幂
    ll res = 1;
    while(b){
        if(b & 1) res = res * a % p;
        a = a * a % p;
        b >>= 1;
    }
    return res;
}

ll C(ll a, ll b, int p){    // 求组合数C(a, b)
    if(a < b) return 0;
    ll fz = 1, fm = 1;    // fz表示分子,fm表示分母
    for(int i = a, j = 1; j <= b; i--, j++){
        fz = fz * i % p;
        fm = fm * j % p;
    }
    return fz * quick_pow(fm, p - 2, p) % p;
    
}

ll Lucas(ll a, ll b, int p){    // Lucas定理
    if(a < p && b < p) return C(a, b, p);
    else return C(a % p, b % p, p) * Lucas(a / p, b / p, p) % p;
}

// ans = Lucas(n, m, p);

四、分解质因数法

分解质因数法是将分子分母的乘积写成质因数乘积,用数组储存指数,最后指数对应相减,剩下来的质因数相乘即为答案。

这里先给出结论C_n^m = \prod_{i = 1}^{k}p_i^{a_i - b_i - c_i},其中k为分解后质因数的个数。

适用范围:需要求的是组合数的真实值而并非取模值时。

证明过程:根据算术基本定理:任何大于1的自然数N,如果N不为质数,那么N可以唯一分解成有限个质数的乘积,即N = {p_1}^{a_1} * {p_2}^{a_2} * ... * {p_n}^{a_n}, \; \; p_1 < p_2 < ... < p_n, \; \; a_1,a_2,...,a_n \in N

由于n!中质因子p出现的次数a = \left \lfloor \frac{n}{p} \right \rfloor + \left \lfloor \frac{n}{p^2} \right \rfloor + ... + \left \lfloor \frac{n}{p^n} \right \rfloor

       m!中质因子p出现的次数b = \left \lfloor \frac{m}{p} \right \rfloor + \left \lfloor \frac{m}{p^2} \right \rfloor + ... + \left \lfloor \frac{m}{p^m} \right \rfloor

       (n - m)!中质因子p出现的次数c = \left \lfloor \frac{n - m}{p} \right \rfloor + \left \lfloor \frac{n - m}{p^2} \right \rfloor + ... + \left \lfloor \frac{n - m}{p^{n - m}} \right \rfloor

所以C_n^m中质因子p出现的次数为a - b - c

C_n^m \in N^*

\frac{n!}{m!(n-m)!}一定可以转换成若干个质因数的乘积,且每个质因数出现的次数为a_i - b_i - c_i

因此最终C_n^m = \prod_{i = 1}^{k}p_i^{a_i - b_i - c_i}得证。

C++代码如下:

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;
const int N = 5005;
int primes[N], cnt;    // cnt[]存储所有素数,cnt表示统计分解成的素数的个数
int sum[N];    // sum[]存储每个素数的次数
bool st[N];    // st[]存储每个数是否被筛掉

void Euler_seive(int n){    // 欧拉筛法求素数
    for(int i = 2; i <= n; i++){
        if(!st[i]) primes[cnt++] = i;
        for(int j = 0; primes[j] <= n / i; j++){
            st[primes[j] * i] = true;
            if(i % primes[j] == 0) break;   // 如果这个数能被素数整除,跳出循环
        }
    }
}

int get(int n, int p){    // 求n!中素数p出现的次数
    int res = 0;
    while(n){
        res += n / p;
        n /= p;
    }
    return res;
}

vector<int> mul(vector<int> a, int b){    // 高精度乘法
    vector<int> c;
    int t = 0;
    for(int i = 0; i < a.size(); i++){
        t += a[i] * b;
        c.push_back(t % 10);
        t /= 10;
    }
    while(t){
        c.push_back(t % 10);
        t /= 10;
    }
    return c;
}

int main(){
    int n, m;
    cin >> n >> m;
    Euler_seive(n);
    for(int i = 0; i < cnt; i++){    // 求每个质因数出现的次数
        int p = primes[i];
        sum[i] = get(n, p) - get(m, p) - get(n - m, p);
    }
    vector<int> ans;
    ans.push_back(1);
    
    for(int i = 0; i < cnt; i++)    // 用高精度乘法将转换后所有剩下的质因数相乘
        for(int j = 0; j < sum[i]; j++)
            ans = mul(ans, primes[i]);
    
    for(int i = ans.size() - 1; i >= 0; i--)    // 输出答案
        cout << ans[i];
    cout << endl;
    return 0;
}