求组合数

求组合数

Method 1 预处理出所有组合数的值

数据范围

询问次数 T = 1 0 5 , 0 ≤ a , b ≤ 1 0 4 T=10^5 ,0\le a,b\le 10^4 T=1050a,b104

求解方法

根据递推式 C a b = C a − 1 b + C a − 1 b − 1 , ( 0 ≤ a , b ≤ 1 0 4 ) C_{a}^{b}=C_{a-1}^{b}+C_{a-1}^{b-1},(0\le a,b\le 10^4) Cab=Ca1b+Ca1b1,(0a,b104)

从实际意义出发理解这个式子,从 a a a 个苹果中选 b b b 个苹果的方案数可以分成两类,从所有苹果中挑出一个苹果 A A A 把所有方案分为包含 A A A 和不包含 A A A 的两类,如果包含 A A A ,方案数是 C a − 1 b − 1 C_{a-1}^{b-1} Ca1b1 ,如果不包含 A A A ,方案数是 C a − 1 b C_{a-1}^{b} Ca1b

Code

void init() {
    for(int i = 0; i < N; i++) { //注意i和j都要从0开始遍历,因为C[0][0]也是有定义的
        for(int j = 0; j <= i; j++) {//因为C[a][b]的a一定大于等于b所以遍历到i就可以
            if(j == 0) c[i][j] = 1;
            else c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % mod;
        }
    }
}

时间复杂度

O ( n 2 ) O(n^2) O(n2)

Method 2 预处理阶乘和阶乘的逆元

数据范围

询问次数 T = 1 0 5 , 0 ≤ a , b ≤ 1 0 5 T=10^5 ,0\le a,b\le 10^5 T=1050a,b105

求解方法

由于
C a b = a ! ( b − a ) ! b ! C_{a}^{b}=\frac{a!}{(b-a)!b!} Cab=(ba)!b!a!
预处理 i i i 的阶乘 m o d   p mod\ p mod p 的值 , i i i 的阶乘的逆元 m o d   p mod \ p mod p 的值

所以
C a b = f a c t ( a ) × i n f a c t ( b − a ) × i n f a c t ( b ) C_{a}^{b}=fact(a)\times infact(b-a)\times infact(b) Cab=fact(a)×infact(ba)×infact(b)

其中, f a c t fact fact 表示 i i i 的阶乘 i n f a c t infact infact 表示 i i i 的阶乘的逆元

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 7, M = N * 2;
const long long mod = 1e9 + 7;

int fact[N], infact[N];

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

int main() {

    //初始化i的阶乘
    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 n;
    scanf("%d", &n);
    while(n--) {
        int a, b;
        scanf("%d%d", &a, &b);
        printf("%d\n", (ll)fact[a] * infact[a - b] % mod * infact[b] % mod);
    }


    return 0;
}

时间复杂度

O ( n l o g n ) O(nlogn) O(nlogn)

Method 3 卢卡斯定理 Lucas

数据范围

n n n 是询问次数, p p p 是模数

1 ≤ n ≤ 20 1≤n≤20 1n20
1 ≤ b ≤ a ≤ 1 0 18 1≤b≤a≤10^{18} 1ba1018
1 ≤ p ≤ 1 0 5 1≤p≤10^5 1p105

求解方法

C a b ≡ ( C a m o d    p b m o d    p × C a ÷ p b ÷ p )   m o d   p C_{a}^{b}\equiv (C_{a\mod {p}}^{b\mod {p}}\times C_{a\div p}^{b\div p}) \ mod \ p Cab(Camodpbmodp×Ca÷pb÷p) mod p

卢卡斯定理的证明

证明

时间复杂度

O ( l o g n N × p × l o g p ) O(log_n^N\times p\times logp) O(lognN×p×logp)

Code

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

int p;

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

ll C(ll a, ll b) {
    ll res = 1;
    for(int i = 1, j = a; i <= b; i++, j--) {
        res = res * j % p;
        res = res * qmi(i, p - 2) % p;
    }
    return res;
}

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

int main() {
    int n;
    cin >> n;
    while(n--) {
        ll a, b;
        cin >> a >> b >> p;
        cout << lucas(a, b) << endl;
    }

    return 0;
}

Method 4 大数运算

数据范围

1 ≤ b ≤ a ≤ 5000 1≤b≤a≤5000 1ba5000

求解方法

根据定义

C a b = a ( a − 1 ) ( a − 2 ) . . . ( a − b + 1 ) b ( b − 1 ) ( b − 2 ) . . . 2 × 1 C_{a}^{b}=\frac{a(a-1)(a-2)...(a-b+1)}{b(b-1)(b-2)...2\times1} Cab=b(b1)(b2)...2×1a(a1)(a2)...(ab+1)

对其分解质因数

p 1 α 1 p 2 α 2 . . . p k α k p_1^{\alpha_1}p_2^{\alpha_2}...p_k^{\alpha_k} p1α1p2α2...pkαk

变成只有乘法的形式,实现高精度乘法

具体步骤如下

  1. 筛素数 筛出 1 ∼ 5000 1\sim 5000 15000 内的素数
  2. 求每个质数的次数

使用 a ! = ⌊ a p ⌋ + ⌊ a p 2 ⌋ + ⌊ a p 3 ⌋ + . . . a!=\lfloor\frac{a}{p}\rfloor+\lfloor\frac{a}{p^2}\rfloor+\lfloor\frac{a}{p^3}\rfloor+... a!=pa+p2a+p3a+...

  1. 用高精度乘法把所有质因子乘到一块去

Code

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

const int N = 1e6 + 7, M = N * 2;

int primes[N], cnt;
int sum[N];//求p的次数
bool st[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] <= 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 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);
    }


    //高精度乘法
    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--) printf("%d",ans[i]);


    return 0;
}

一些例题

卡特兰数

889. 满足条件的01序列

把每一个序列转化为一条路径, 0 表示向右走一格,1 表示向上走一格,那么任何前缀中 0 的个数不少于 1 的个数就转化为,路径上的任意一点,横坐标大于等于纵坐标。题目所求即为这样的合法路径数量。

下图中,表示从 ( 0 , 0 ) (0,0) (0,0) 走到 ( n , n ) (n,n) (n,n) 的路径,在绿线及以下表示合法,若触碰红线即不合法。

把每一个序列转化为一条路径, 0 表示向右走一格,1 表示向上走一格,那么任何前缀中 0 的个数不少于 1 的个数就转化为,路径上的任意一点,横坐标大于等于纵坐标。题目所求即为这样的合法路径数量。

下图中,表示从 ( 0 , 0 ) (0,0) (0,0) 走到 ( n , n ) (n,n) (n,n) 的路径,在绿线及以下表示合法,若触碰红线即不合法。

Catalan.png
原链接

  • ( 0 , 0 ) (0,0) (0,0) 走到 ( n , n ) (n,n) (n,n) 的路径一共有 2 n 2n 2n 种走法,在其中选择 n n n 种走法,总共的方案数就是 C 2 n n C^{n}_{2n} C2nn

  • 对于任何一个经过红颜色边的路径,把第一次经过红颜色边之后的路径做轴对称,终点一定是 ( n − 1 , n + 1 ) (n-1,n+1) (n1,n+1) ,不合法的方案数为, C 2 n n − 1 / C 2 n n + 1 C^{n-1}_{2n}/C^{n+1}_{2n} C2nn1/C2nn+1

  • 最终结果为 a n s = C 2 n n − C 2 n n − 1 = C 2 n n − 1 n + 1 ans=C^{n}_{2n}-C^{n-1}_{2n}=\frac{C^{n-1}_{2n}}{n+1} ans=C2nnC2nn1=n+1C2nn1

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const long long mod = 1e9 + 7;

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

int main() {
	ll n;
	cin >> n;

	ll res = 1;
	ll a = 2 * n;
	ll b = n;

	for(int i = a; i >= a - b + 1; i--) res = res * i % mod;
	for(int i = 1; i <= b; i++) res = res * qmi(i, mod - 2, mod) % mod;

	res = (res * (n + 1))% mod;

	cout << res;

	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值