算法学习系列(三十一):组合数

目录

  • 引言
  • 一、 C n m C_n^m Cnm
  • 二、递推
    • 1.公式
    • 2.例题
  • 三、预处理
    • 1.公式
    • 2.例题
  • 四、卢卡斯定理
    • 1.公式
    • 2.例题
  • 五、高精度计算
    • 2.例题

引言

这个组合数的问题还是很常见的,就是问 C n m C_n^m Cnm,然后会根据询问的次数,以及n和m的大小来判断怎么去做。本文用了一写基本的数论公式来进行推导,包括卢卡斯定理等,进行计算。

一、 C n m C_n^m Cnm

C n m = n ⋅ ( n − 1 ) ⋅ ( n − 2 ) ⋯ ( n − m + 1 ) m ⋅ ( m − 1 ) ⋅ ( m − 2 ) ⋯ 1 = n ! m ! ( n − m ) ! C_n^m=\frac{n\cdot(n-1)\cdot(n-2)\cdots(n-m+1)}{m\cdot (m-1)\cdot (m-2)\cdots 1}=\frac{n!}{m!(n-m)!} Cnm=m(m1)(m2)1n(n1)(n2)(nm+1)=m!(nm)!n!

实际意义:从n件物品中,取m件,总共有多少种取法(不区分取的先后顺序)

以下是根据n,m,p所决定用哪一种方法
算法学习系列(三十一):组合数_第1张图片

二、递推

1.公式

C n m = C n − 1 m + C n − 1 m − 1 C_n^m=C_{n-1}^m+C_{n-1}^{m-1} Cnm=Cn1m+Cn1m1
理解:把这n份物品分为1和n-1,那么所有的取法为:取这个1和不取这个1,即公式

2.例题

题目描述:

给定 n 组询问,每组询问给定两个整数 a,b,请你输出 Cba mod(109+7) 的值。

输入格式
第一行包含整数 n。
接下来 n 行,每行包含一组 a 和 b。

输出格式
共 n 行,每行输出一个询问的解。

数据范围
1≤n≤10000,1≤b≤a≤2000
 
输入样例:
3
3 1
5 3
2 2
输出样例:
3
10
1

代码示例:

#include 
#include 

using namespace std;

const int N = 2010, mod = 1e9+7;

int c[N][N];

void init()  // 预处理
{
    for(int i = 0; i < N; ++i)
    {
        for(int j = 0; j <= i; ++j)
        {
            if(!j) c[i][j] = 1;  // 当j为0 c[i][j]必为1
            else c[i][j] = (c[i-1][j] + c[i-1][j-1]) % mod;  // 公式
        }
    }
}

int main()
{
    int n;
    scanf("%d", &n);
    
    init();
    
    while(n--)
    {
        int n, m;
        scanf("%d%d", &n, &m);
        
        printf("%d\n", c[n][m]);
    }
    
    return 0;
}

三、预处理

1.公式

C n m = n ! m ! ( n − m ) ! C_n^m=\frac{n!}{m!(n-m)!} Cnm=m!(nm)!n! 由于除法不方便则用逆元代替: C n m ≡ n !   ⋅   ( m ! ) − 1   ( n − m ) ! − 1 ( m o d p ) 由于除法不方便则用逆元代替:C_n^m \equiv n!\ \cdot\ (m!)^{-1}\ (n-m)!^{-1} \pmod p 由于除法不方便则用逆元代替:Cnmn!  (m!)1 (nm)!1(modp)求逆元可以参考我之前写的博客快速幂、逆元

2.例题

题目描述:

给定 n 组询问,每组询问给定两个整数 a,b,请你输出 Cbamod(109+7) 的值。

输入格式
第一行包含整数 n。
接下来 n 行,每行包含一组 a 和 b。

输出格式
共 n 行,每行输出一个询问的解。

数据范围
1≤n≤10000 ,1≤b≤a≤105

输入样例:
3
3 1
5 3
2 2
输出样例:
3
10
1

代码示例:

#include 
#include 

using namespace std;

typedef long long LL;

const int N = 1e5+10, mod = 1e9+7;

int fact[N], infact[N];  //阶乘 阶乘的逆元

int qmi(int a, int k, int p)
{
    int res = 1;
    while(k)
    {
        if(k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    
    return res;
}

void init()
{
    fact[0] = infact[0] = 1;
    for(int i = 1; i < N; ++i)
    {
        fact[i] = (LL)fact[i-1] * i % mod;
        infact[i] = (LL)infact[i-1] * qmi(i, mod - 2, mod) % mod;
    }
}

int main()
{
    int n;
    scanf("%d", &n);
    
    init();
    
    while(n--)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        
        printf("%d\n", (LL)fact[a] * infact[b] % mod * infact[a-b] % mod);
    }
    
    return 0;
}

四、卢卡斯定理

1.公式

卢卡斯定理: C n m   ≡   C n   m o d   p m   m o d   p   ⋅   C n / p m / p ( m o d p ) 卢卡斯定理:C_n^m\ \equiv\ {C_{n\ mod\ p}^{m\ mod\ p}\ \cdot\ C_{n/p}^{m/p}} \pmod{p} 卢卡斯定理:Cnm  Cn mod pm mod p  Cn/pm/p(modp) C n m ≡ n !   ⋅   ( m ! ) − 1   ( n − m ) ! − 1 ( m o d p ) C_n^m \equiv n!\ \cdot\ (m!)^{-1}\ (n-m)!^{-1} \pmod p Cnmn!  (m!)1 (nm)!1(modp)

2.例题

题目描述:

给定 n 组询问,每组询问给定三个整数 a,b,p,其中 p 是质数,请你输出 Cbamodp 的值。

输入格式
第一行包含整数 n。
接下来 n 行,每行包含一组 a,b,p。

输出格式
共 n 行,每行输出一个询问的解。

数据范围
1≤n≤20,1≤b≤a≤1018 ,1≤p≤105,
输入样例:
3
5 3 7
3 1 5
6 4 13
输出样例:
3
3
2

代码示例:

#include 
#include 

using namespace std;

typedef long long LL;

int qmi(int a, int k, int p)
{
    int res = 1;
    while(k)
    {
        if(k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    
    return res;
}

int C(int a, int b, int p)
{
    if(b > a) return 0;
    
    int res = 1;
    for(int i = a, j = b; j >= 1; i--, j--)
    {
        res = (LL)res * i % p;
        res = (LL)res * qmi(j, p-2, p) % p;
    }
    
    return res;
}

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

int main()
{
    int n;
    scanf("%d", &n);
    
    while(n--)
    {
        LL a, b;
        int p;
        scanf("%lld%lld%d", &a, &b, &p);
        printf("%d\n", Lucas(a,b,p));
    }
    
    return 0;
}

五、高精度计算

思想: C a b C_a^b Cab分解质因数,然后用高精度乘法将每次的结果与分解的质因数相乘。
高精度乘法可参考我之前的博客高精度乘法
C a b = a ! b !   ⋅   ( a − b ) ! C_a^b=\frac{a!}{b!\ \cdot\ (a-b)! } Cab=b!  (ab)!a! a ! 中含质因子 p 的个数: a p + a p 2 + a p 3 + ⋯ ( 除法皆为下取整 ) a!中含质因子p的个数:\frac{a}{p}+\frac{a}{p^2}+\frac{a}{p^3}+\cdots (除法皆为下取整) a!中含质因子p的个数:pa+p2a+p3a+(除法皆为下取整)
如何分解质因数:可以先求出来 a !   m o d   p a!\ mod\ p a! mod p的质因数的个数,再求出 b ! 和 ( a − b ) ! 的个数,两个相减就可以了 b!和(a-b)!的个数,两个相减就可以了 b!(ab)!的个数,两个相减就可以了

2.例题

题目描述:

输入 a,b,求 Cba 的值。

注意结果可能很大,需要使用高精度计算。

输入格式
共一行,包含两个整数 a 和 b。

输出格式
共一行,输出 Cba 的值。

数据范围
1≤b≤a≤5000
输入样例:
5 3
输出样例:
10

代码示例:

#include 
#include 
#include 

using namespace std;

const int N = 5010;

int primes[N], cnt;
bool st[N];
int sum[N]; //质因子的个数

void get_primes(int n)
{
    for(int i = 2; i <= n; ++i)
    {
        if(!st[i]) primes[cnt++] = i;
        for(int j = 0; primes[j] * i <= n; ++j)
        {
            st[primes[j] * i] = true;
            if(i % primes[j] == 0) break;
        }
    }
}

vector<int> mul(vector<int>& a, int b)
{
    vector<int> res;
    
    int t = 0;
    for(int i = 0; i < a.size() || t; ++i)
    {
        if(i < a.size()) t += a[i] * b;
        res.push_back(t % 10);
        t /= 10;
    }
    
    while(res.size() > 1 && res.back() == 0) res.pop_back();
    
    return res;
}

int get(int n, int p) // 求n!中质因子p的个数
{
    int res = 0;
    while(n)
    {
        res += n / p;
        n /= p;
    }
    
    return res;
}

int main()
{
    int a, b;
    cin >> a >> b;
    
    get_primes(a);
    
    for(int i = 0; i < cnt; ++i)
    {
        int p = primes[i];
        sum[i] = get(a, p) - get(b, p) - get(a-b, p);  // 求包含质因子primes[i]的个数
    }
    
    vector<int> res;
    res.push_back(1);
    for(int i = 0; i < cnt; ++i)
    {
        for(int j = 0; j < sum[i]; ++j)
        {
            res = mul(res, primes[i]);  // 用高精度与每一个质因子相乘
        }
    }
    
    for(int i = res.size() - 1; i >= 0; --i) printf("%d", res[i]);  // 输出结果
    
    return 0;
}

你可能感兴趣的:(算法,算法,学习)