AcWing 887 求组合数 III

题目描述:

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

输入格式

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

输出格式

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

数据范围

1≤n≤20,1≤b≤a≤10^18,1≤p≤10^5,

输入样例:

3
5 3 7
3 1 5
6 4 13

输出样例:

3
3
2

分析:

本题a和b的值都比较大,可以使用卢卡斯定理来做。其实我们只需要记住一个结论:C(a,b) mod p = C(a mod p,b mod p) * C(a / p,b / p) mod p即可解决本题。也可以偷懒不去看卢卡斯定理的证明,因为看了也不一定看得懂。但是本着求真务实的精神,还是证一下吧。先把最常规的证明贴上来:

下面要做的是解释上面图片的推导过程,首先看见a和b都被展开成一个质数p的多项式可能有点懵,其实只需要理解为p进制数即可,举个例子,a = 107,p = 7,107 = 107 % 7 + 107 / 7 * 7 = 2 + 105 = 2 + 7 + 49 * 2 = 2 * 7^0 + 1 * 7^1 + 2 * 7^2.我们先假设卢卡斯定理的这个结论:成立,C(a,b) mod p = C(a mod p,b mod p) * C(a / p,b / p) mod p相当于之前结论的递推式,就像107固然可以展开成2 + 7 + 49*2,也可以展开成107 % 7 + 107 / 7 * 7一样。

然后是的结论,理解为C(p,j)中肯定包含p这个因子,因此模上p为0,。由于展开式中间都含有因子p,故模上p后留下的只有1和x^p,这个式子是将a展开成p进制多项式后展开,调用之前的结论得出的。下面的最后一步,也是关键,左边x^b的系数为C(a,b),(1+x)^a0中x^b0的系数为C(a0,b0),...,(1+x^(p^k))^ak中x^bkp^k的系数为C(ak,bk),故C(a0,b0)x^b0 * ... * C(ak,bk)x^bkp^k = C(a0,b0)* ... * C(ak,bk)x^(b0+...+bkp^k) =C(a0,b0)* ... * C(ak,bk)x^b。故 。如果右边某项bi > ai说明C(a,b) = 0,右边无法凑出x^k的系数。

下面正式运用卢卡斯定理解决本题。C(a,b) mod p = C(a mod p,b mod p) * C(a / p,b / p) mod p,a、b都小于p时,直接计算C(a,b),否则递归的调用卢卡斯定理。具体的C(a,b)= a * (a - 1) *... (a - b + 1) / b!,除法运算同样改为乘上乘法逆元的操作。

复杂度分析:求C(a,b)操作耗时是p的字长log10^5约等于60,lucas递归函数的复杂度为logpa * log2p * n,a最大取10^18约等于2^60,p = 2时,logpa约等于60,n最大取20,一共是1200,p = 10^5时,logpa约等于4,一共是4800,当然p取中间某数时执行次数可能超过1w,但是肯定可以在1s内完成。

#include <iostream>
#include <algorithm>
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 = 1,j = a;i <= b;i++,j--){
        res = (ll)res * j % p;
        res = (ll)res * qmi(i,p - 2,p) % p;
    }
    return res;
}
int lucas(ll a,ll b,int p){
    if(a < p && b < p)  return C(a,b,p);
    return (ll)C(a % p,b % p,p) * lucas(a / p,b / p,p) % p;
}
int main(){
    int n,p;
    ll a,b;
    cin>>n;
    while(n--){
        cin>>a>>b>>p;
        cout<<lucas(a,b,p)<<endl;
    }
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值