求组合数的四种方法以及卡特兰数

文章目录

  • 组合数范围较小 && 模量一定
    • 方法 - 递推法
    • 思路
    • 时间复杂度分析
    • AcWing 885. 求组合数 I
      • CODE
  • 组合数范围较大 && 模量一定
    • 方法 - 快速幂
    • 时间复杂度分析
    • AcWing 886. 求组合数 II
      • CODE
  • 组合数范围爆大 && 模量不定
    • 方法 - Lucas定理
    • 时间复杂度分析
    • AcWing 887. 求组合数 III
      • CODE
  • 组合数范围爆大 && 没有模量
    • 方法 - 线性筛 + 高精度
    • 时间复杂度分析
    • AcWing 888. 求组合数 IV
      • CODE
  • 卡特兰数(Catalan Number)
    • 介绍
    • 证明
    • AcWing 889. 满足条件的01序列
      • CODE



组合数范围较小 && 模量一定

方法 - 递推法

直接递推出每个组合数的值进行打表,然后问到就查表。


思路

需要用到递推式: C a b = C a − 1 b − 1 + C a − 1 b C^b_a = C^{b- 1}_{a - 1} + C^{b}_{a - 1} Cab=Ca1b1+Ca1b
这个式子意思是:从 a 中 选 b 个人 = 对于一个人 k :(选中 k 后还需要在剩下的 a - 1 个里面再选 b - 1 个,不选 k 还需要在剩下 a - 1 个人里面选出 b 个)。


时间复杂度分析

0 0 0 递推到 n n n,而且每个 n n n 都要从 0 0 0 递推到 n n n,所以是: O ( n 2 ) O(n^2) O(n2)


AcWing 885. 求组合数 I

题目链接:https://www.acwing.com/activity/content/problem/content/955/

求组合数的四种方法以及卡特兰数_第1张图片

CODE

#include 
#include 
#include 

using namespace std;

typedef long long ll;  // 定义长整型别名为ll

const int N = 2010, mod = 1e9 + 7;  // 定义常量N和mod
ll c[N][N];  // 定义二维数组c

int main()
{
    // 初始化二维数组c
    for(int i = 0; i <= 2000; ++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 - 1] + c[i - 1][j]) % mod;  // 计算组合数
        }
    }
    
    int n;
    scanf("%d", &n);  // 输入测试用例数量
    while (n -- ){
        int a, b;
        scanf("%d%d", &a, &b);  // 输入a和b
        
        printf("%d\n", c[a][b]);  // 输出c[a][b]
    }
}


组合数范围较大 && 模量一定

方法 - 快速幂

此时使用递推法肯定会TLE,于是我们从定义出发求 C n m = n ! ( n − m ) ! m ! C_n^m = \frac{n!}{(n - m)!m!} Cnm=(nm)!m!n!,我们需要用快速幂求各阶乘与阶乘在模意义下的逆元(当然前提得是模量得是质数或者二者互质)。
根据费马小定理,可以得到阶乘逆元公式: i n f a c [ i ] = i n f a c [ i − 1 ] ∗ q m i ( i , m o d − 2 , m o d ) infac[i] = infac[i - 1] * qmi(i, mod - 2, mod) infac[i]=infac[i1]qmi(i,mod2,mod)
就是上一个数的逆元再乘上用快速幂求得的这个数的逆元。


时间复杂度分析

我们预处理了每个数的阶乘与阶乘的逆元,所以复杂度就是它俩相乘: O ( n ⋅ l o g ( m o d ) ) O(n·log(mod)) O(nlog(mod))


AcWing 886. 求组合数 II

题目链接:https://www.acwing.com/activity/content/problem/content/956/

求组合数的四种方法以及卡特兰数_第2张图片

CODE

#include 
#include 
#include 

using namespace std;

typedef long long ll;  // 定义长整型别名为ll

const int mod = 1e9 + 7, N = 1e5 + 5;  // 定义常量mod和N
ll fac[N], infac[N];  // 定义一维数组fac和infac

// 快速幂运算函数
int qmi(int a, int b, int m){
    int res = 1;
    while(b){
        if(b & 1 == 1) res = (ll)res * a % m;  // 如果b是奇数,则累乘到结果中
        a = (ll)a * a % m;  // a自身平方
        b >>= 1;  // b右移一位,相当于除以2
    }
    
    return res;
}

// 初始化函数
void init(){
    fac[0] = infac[0] =1;
    for(int i = 1; i < N; ++i){
        fac[i] = fac[i - 1] * i % mod;  // 计算阶乘
        infac[i] = (ll)infac[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);  // 输入a和b
        
        // 输出组合数C(a, b),即a个数中选b个的组合数
        printf("%d\n", (ll)fac[a] * infac[b] % mod * infac[a - b] % mod);
    }
}


组合数范围爆大 && 模量不定

方法 - Lucas定理

L u c a s 定理: C n m = C n / p m / p ⋅ C n    m o d   p m   m o d   p   ( m o d   p ) Lucas定理:\\C_n^m = C_{n / p}^{m / p}·C_{n\ \ mod\ p}^{m\ mod\ p}\ (mod\ p) Lucas定理:Cnm=Cn/pm/pCn  mod pm mod p (mod p)
这样我们就可以将组合数 C n    m o d   p m   m o d   p C_{n\ \ mod\ p}^{m\ mod\ p} Cn  mod pm mod p 范围控制在较小的模量内,至于 C n / p m / p C_{n / p}^{m / p} Cn/pm/p,我们可以将其再继续递归使用 L u c a s Lucas Lucas 定理求解。


时间复杂度分析

我们可以预先打表求阶乘以及阶乘逆元,那么复杂度就是 O ( p ⋅ l o g p ) O(p·logp) O(plogp)
也可以直接循环求解不打表,对于询问数较少的可以这样求,各有各的好处。


AcWing 887. 求组合数 III

题目链接:https://www.acwing.com/activity/content/problem/content/957/

求组合数的四种方法以及卡特兰数_第3张图片

CODE

#include 
#include 
#include 

using namespace std;
typedef long long ll;

// 快速幂运算函数,计算a的b次方对p取模的结果
ll qmi(ll a, ll b, ll p){
    ll res = 1;
    while(b){
        if(b & 1 == 1) res = res * a % p;  // 如果b是奇数,则累乘到结果中
        a = a * a % p;  // a自身平方
        b >>= 1;  		// b右移一位,相当于除以2
    }
    return res;
}

// 计算组合数C(a, b)对p取模的结果
ll C(ll a, ll b, ll p){
    ll res = 1;
    for(int i = 1, j = a; i <= b; ++i, --j){
        res = res * j % p;  // 计算阶乘部分
        res = res * qmi(i, p - 2, p) % p;  // 计算阶乘的逆元部分
    }
    
    return res;
}

// Lucas定理,用于计算大组合数取模的问题
ll lucas(ll a, ll b, ll p){
    if(a < p && b < p) return C(a, b, p);  // 如果a和b都小于p,直接计算C(a, b)
    else return C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;  // 否则,递归计算
}

int main()
{
    int n;
    scanf("%d", &n);  // 输入测试用例数量
    while (n -- ){
        ll a, b, p;
        scanf("%lld%lld%lld", &a, &b, &p);  // 输入a, b, p
    
        printf("%d\n", lucas(a, b, p));  	// 输出lucas(a, b, p)的结果
    }
}

参考题解:https://www.acwing.com/solution/content/5244/,这篇解释了C()函数的由来,懒得打字了。



组合数范围爆大 && 没有模量

方法 - 线性筛 + 高精度

我们需要需处理出 C n m C_n^m Cnm 中包含的质因子的底数以及指数,然后利用高精度乘法求出值。


时间复杂度分析

线性筛是 O ( n ) O(n) O(n),但是高精度的步骤我实在推不出来啊 我是采购啊啊啊,什么都不会呜呜 >_<


AcWing 888. 求组合数 IV

题目链接:https://www.acwing.com/activity/content/problem/content/958/

求组合数的四种方法以及卡特兰数_第4张图片

CODE

#include 
#include 
#include 
#include 

using namespace std;

const int N = 5010;
int primes[N], cnt;  // primes数组存储所有素数,cnt为素数的个数
int sum[N];  // sum数组用于存储结果
bool st[N];  // st数组用于标记是否为素数

// 筛选素数
void get_primes(int n){
    for(int i = 2; i <= N; ++i){
        if(!st[i]) primes[cnt++] = i;  	// 如果i是素数,则加入到primes数组中
        for(int j = 0; primes[j] <= N / i; ++j){
            st[i * primes[j]] = true;  	// 标记i * primes[j]不是素数
            if(i % primes[j] == 0) break;  // 如果i能被primes[j]整除,则跳出循环
        }
    }
}

// 计算a中包含多少个p
int get(int a, int p){
    int res = 0;
    while(a){
        res += a / p;  // a中包含多少个p
        a /= p;  // a除以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 a, b;
    scanf("%d%d", &a, &b);  // 输入a和b
    
    get_primes(a);  // 筛选素数
    
    for(int i = 0; i < cnt; ++i){
        int p = primes[i];
        sum[i] = get(a, p) - get(a - b, p) - get(b, p);  // 计算a, a - b, b中包含多少个p
    }
    
    vector<int> res;
    res.push_back(1);  // 初始化结果为1
    
    for(int i = 0; i < cnt; ++i){			// 枚举每一个质数
        for(int j = 0; j < sum[i]; ++j){	// 循环primes[i]的指数次
            res = mul(res, primes[i]);  	// 将结果乘以primes[i]
        }
    }
    
    for(int i = res.size() - 1; i >= 0; -- i){
        printf("%d", res[i]);  // 输出结果
    }
    puts("");  // 输出换行
}


卡特兰数(Catalan Number)

介绍

从网格图的左下走到右上,只能往右往上走,而且往上走的步数不能超过往右走的步数,也就是路线不能超过绿线,那么所有的走法就是卡特兰数。

而卡特兰数的通项公式即为: ( Ⅰ ) C a t n = C 2 n n − C 2 n n − 1 ,   ( Ⅱ ) C a t n = 1 n + 1 C 2 n n ,   ( Ⅲ ) C a t n = 4 n + 2 n + 2 C a t n − 1 (Ⅰ)Cat_n = C_{2n}^n - C_{2n}^{n - 1},\ (Ⅱ)Cat_n = \frac{1}{n + 1}C_{2n}^n,\ (Ⅲ)Cat_n = \frac{4n + 2}{n + 2}Cat_{n - 1} ()Catn=C2nnC2nn1, ()Catn=n+11C2nn, ()Catn=n+24n+2Catn1
求组合数的四种方法以及卡特兰数_第5张图片


证明

Ⅰ、Ⅱ式的具体证明请看 VCR:卡特兰数
Ⅲ式详细证明可以看博客:递推式

Ⅰ式 C a t n = C 2 n n − C 2 n n − 1 Cat_n = C_{2n}^n - C_{2n}^{n - 1} Catn=C2nnC2nn1 思路就是:拿所有路线减去错误路线,那么全部路线好求,错误路线该怎么求呢?

  • 我们发现所有错误路线必然经过将绿线向上平移一格的红线
    求组合数的四种方法以及卡特兰数_第6张图片
  • 所以说所有错误路线的终点就是原终点关于红线对称的点 ( n − 1 , n + 1 ) (n - 1, n + 1) (n1,n+1),那么我们统计有多少条路线能走到这个点就可以了,所以是 C 2 n n − 1 C_{2n}^{n - 1} C2nn1

Ⅱ式由Ⅰ式化简可得。

Ⅲ式是递推得来的:
求组合数的四种方法以及卡特兰数_第7张图片


AcWing 889. 满足条件的01序列

题目链接:https://www.acwing.com/activity/content/problem/content/959/

求组合数的四种方法以及卡特兰数_第8张图片

CODE

#include 
#include 
#include 

using namespace std;

typedef long long ll;  // 定义长整型别名

const int mod = 1e9 + 7;  // 定义模数

// 快速幂算法,计算a的b次方对p取模的结果
int qmi(int a, int b, int p){
    int res = 1;
    while(b){
        if(b & 1 == 1) res = (ll)res * a % mod;  // 如果b是奇数,则乘上a
        a = (ll)a * a % mod;  // a自身平方
        b >>= 1;  	// b右移一位,相当于除以2
    }
    
    return res;
}

int main()
{
    int n;
    scanf("%d", &n);  // 输入一个整数n
    
    int a = 2 * n, b = n;
    int res = 1;
    
    // 计算分子部分,即(a*(a-1)*...*(a-b+1))对mod取模的结果
    for(int i = a; i > a - b; --i) res = (ll)res * i % mod;
    
    // 计算分母部分,即(b*(b-1)*...*1)对mod取模的结果,并求其逆元
    for(int i = 1; i <= b; ++i) res = (ll)res * qmi(i, mod - 2, mod) % mod;
    
    // 最后再乘上(n+1)的逆元
    res = (ll) res * qmi(n + 1, mod - 2, mod) % mod;
    
    cout << res << endl;  // 输出结果
}

此处需要额外注意:

素材来自:题解代码的评论区。

你可能感兴趣的:(算法学习记录,算法,笔记,c++)