组合数的三种常规精度求法

本节介绍组合数的三种常规精度求法,用以应对不同类型的数据

看一下第一种数据类型

file

分析数据范围发现特点是ab都较小,问询数较多 这种数据比较适合用DP打表预处理出所有的Cab,然后根据问询输出

递推使用公式

{n \choose k } = {n - 1 \choose k - 1} + {n - 1 \choose k}

一个感性好用的理解: 对于要从n个苹果里选出k个的选法,由两种情况构成:对于任意一个苹果,我们可以选择,然后从n – 1个苹果里选k – 1个,或者不选,即从 n – 1个苹果里选k个

直接DP,初始化出边界为1的情况然后推出一个三角形区域即可 如果不理解用Excel画一下会很有帮助 file

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

const int N = 2010, MOD = 1e9 + 7;
int c[N][N];
void init()
{
    // C[a][b] = C[a - 1][b - 1] + C[a][b - 1];
    for(int i = 0; i < N; i++)
        c[0][i] = 1;
    for(int i = 1; i < N; i++)
        for(int j = i; j < N; j++)
            c[i][j] = (c[i - 1][j - 1] + c[i][j - 1]) % MOD;
}

int main()
{
    // ios::sync_with_stdio(false);
    int n;
    scanf("%d", &n);

    init();

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

需要特别注意的一点是,我在写Cab时,ab的顺序比较随意,看代码时请留意一下

看一下第二种数据类型

file

显然无法处理出所有Cab来打表解决问题

此时可以考虑从公式方面解决

{a \choose b} = \frac{a!}{b!(a-b)!} 

而题目要求结果对1e9+7取模,可以考虑打表活的范围内ab的阶乘和ab mod p的逆,以此来解决问题

需要注意的是,本题模数p是质数,所以求逆首选快速幂(费马小定理)法,不了解乘法逆元和费马小定理的可以看我这篇文章

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

const ll MOD = 1e9 + 7;
const int N = 1e5 + 10;

ll fact[N], infact[N]; // 分别储存n的阶乘和n的阶乘mod MOD的逆

ll qmi(ll x, ll q, ll m)
{
    // x ^ q % m, 快速幂模板
    ll res = 1;
    while (q)
    {
        if (q & 1)
            res = (res * x % m) % m;
        q >>= 1;
        x = (x * x % m) % m;
    }
    return res;
}

void init()
{
    // 出始化出n的阶乘和n的阶乘mod MOD的逆
    // 1 <= n <= 1e5
    ll x = 1;
    fact[0] = 1, infact[0] = 1;
    for (int i = 1; i <= (int)1e5; i++)
    {
        x = (x * i) % MOD;
        fact[i] = x;
        infact[i] = qmi(x, MOD - 2, MOD);
    }
}

ll getC(ll a, ll b)
{
    // C(b, a) = a! / (b! (a- b)!)
    return ((fact[a] * infact[b] % MOD) * infact[a - b] % MOD) % MOD;
}

int main()
{
    //ios::sync_with_stdio(false);
    init();
    int n;
    scanf("%d", &n);
    while (n--)
    {
        ll a, b;
        scanf("%lld %lld", &a, &b);
        printf("%lld\n", getC(a, b));
    }
    return 0;
}

来看第三种数据类型

file这种数据类型的特点是模数很小,ab都很大 此时需要用到Lucas定理来对数据的范围进行处理 Lucas定理/卢卡斯定理:

{a \choose b} \equiv {a\%p \choose b\%p} \cdot {{\left \lfloor \frac{a}{p}  \right \rfloor }  \choose{{\left \lfloor \frac{b}{p}  \right \rfloor }}} \quad (mod \quad p)

证明这里不关心,有兴趣可以b站搜索一下相关证法

利用Lucas定理可以将问题规模变小,方便解题 在写代码时候要特别注意取模问题,因为数据很大,相乘时候非常容易溢出

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

ll qmi(ll a, ll k, ll p)
{
    // a ^ p % q, 快速幂模板
    ll res = 1;
    while (k)
    {
        if (k & 1)
            res = (res * a % p) % p;
        k >>= 1;
        a = (a * a % p) % p;
    }
    return res;
}

ll C(ll a, ll b, ll p)
{
    // C(a, b) = a! / (b!(a - b)!) = (a * (a - 1) * (a - 2) * ... * (a - b + 1)) / b!
    if(b > a)    return 0; //这一行特判加上, 下一行其实可有可无
    if(b == a)  return 1;
    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;
}

ll lucas(ll a, ll b, ll p)
{
    // 递归处理
    if(a < p && b < p)
        return C(a, b, p);
    // 对p取模后肯定都小于p了,所以用C, 而整除p之后不一定小于p, 还需要递归
    // return的取模非常麻烦, 但是一定要注意
    return (C(a % p, b % p, p) * lucas(a / p, b / p, p) % p) % p;
}

int main()
{
    //ios::sync_with_stdio(false);
    int n;
    cin >> n;
    while (n--)
    {
        ll a, b, p;
        cin >> a >> b >> p;
        cout << lucas(a, b, p) << endl;
    }
    return 0;
}

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注