今天在刷OJ的时候,刷到了这样一道题
题目描述:
NEUQ-AcmClub养了许多鸽子,有一天鸽子王想给鸽子们排排班,现在有n只鸽子,每天需要m只鸽子值班,问有多少种值班组合。
由于答案可能较大,我们把答案对一个素数p取模
输入:
输入三个整数 m,n, p m,n<10^18,p <=10^5
输出:
输出一个整数
样例输入:
2 5 11
样例输出
10
这一看不就是个水题直接求组合数求余嘛,打表直接提交。
memset(c,0,sizeof(c));
c[0][0] = 1;
for(int i = 1; i <= 1000; i ++){
for(int j = 0; j <= i; j ++)
if(j == 0 || j == i) c[i][j] = 1;
else c[i][j] = c[i-1][j-1] + c[i-1][j];
}
成功WA,返回来一看,m,n的数据范围直接给到了int_64,思考一番,不会。果断去百度了一下,才发现原来有一个定理——卢卡斯定理(用来特意解决这类问题),可能是我比较菜,费了好大劲才理解网上的讲解,于是打算自己写一篇我认为相对来说易于理解的blog。
(由于是主要学习算法,所以博客在分析算法的为主线的情况下去理解各种定理)
首先,我们目的是要求C(n,m)%p,而卢卡斯定理就是用来解决组合数求余的问题,首先来看一下卢卡斯定理的定义
通俗来讲就是
前提:
a,b可以表示成
且p为素数
结论:
前提对于任意a,b和素数p一定成立,因为最简形式为a = a0,如果a或b小于p,则问题退化为求c(a,b)
至于证明,可以简单了解一下,涉及到的数学知识较多,并不是算法的重点,所以不再过多介绍
下图参考冯志刚《初等数论》第37页。
有了卢卡斯定理,我们的求解过程就变得简单了。
Lucas:
return 求组合数(a % p, b % p) * 卢卡斯函数(a / p, b / p);
那么如何来理解上面的代码呢,首先看求组合数这个函数,传入的是(a%p,b%p)这两个参数,那么正好对应上述公式里的a0和b0,然后将a,b分别除于p相当于以下过程
所以每次相当于将上述公式的ai传进去了,直到a被除到0为止
解决了这个问题,看似已经解决了求组合数的问题,但是还有很重要的一点,就是求余,在int_64的数据范围下,不采用同余法,是非常容易让数据直接爆掉的,而求组合数公式为
C(n, m) mod p = n!/(m!(n - m)!) mod p
不巧,恰好用到了除法,不能使用同余定理,但是不使用的话又会爆数据,那怎么办呢?这时候就要引入一个新的定理和新的概念了。新的概念就是乘法逆元。
乘法逆元:
a * b≡ 1 mod c 若 (a*b)%c ≡ 1成立,则称a关于模c的乘法逆元为b,反之同样成立。
这样有什么用处呢?引入乘法逆元之后 若 a*b≡1mod c 则 (d / a)%c ≡(d * b)%c (逆元就相当于倒数 a除以b就相当于a乘b分之一) 这时候求组合数的表达式就可以转化为乘法,也就可以使用同余定理了。
知道逆元的存在,所有问题都迎刃而解,但是应该怎么求逆元呢?
这时候在引入一个新的定理——费马小定理
当p为质数时 a^(p-1) ≡ 1 (mod p)
将等式变形一下 a * a^(p-2) ≡ 1 (mod p)所以 a 关于模p的逆元为a ^(p-2)
此时所有问题都影刃而解,可以码代码了,但是开头提到的扩展欧几里得和这些又有什么关系呢?
扩展欧几里得是另一种求逆元的方法,ax≡1 (mod p)可以变形为ax-yp=1 在a,b已知情况下,可以用扩展欧几里得算法算出x,求出a的逆元。(求解组合数时仍采用费马小定理,个人认为比较容易理解)
这里给出扩展欧几里得算法:
void gcd(int a,int b,int &d,int &x,int &y){
if(!b){ d=a;x=1;y=0 }
else{ gcd(b,a%b,d,y,x); y-= x*(a/b); }
}
有兴趣可以自己了解一下
那么继续回到主题,所有的准备工作已经就绪,接下来具体实现的代码:
#include
using namespace std;
typedef long long ll;
ll quick_mod(ll a,ll b,ll m)//快速幂求a^b
{
ll ans = 1;
while(b!=0){
if(b&1!=0) ans = ((ans % m) * (a % m)) % m;
a = ((a % m) * (a % m)) % m;
b >>= 1;
}
return ans;
}
ll comp(ll a,ll b,ll m) //求组合数
{
if(a<b) return 0;
if(a==b) return 1;
if(b>a-b) b=a-b;
ll ans=1,ca=1,cb=1;
for(int i=0;i<b;i++){
ca=ca*(a-i)%m;
cb=cb*(b-i)%m;
}
ans=ca * quick_mod(cb,m-2,m) % m; //quick_mod(cb,m-2,m)费马小定理求逆元
return ans;
}
ll lucas(ll a,ll b,ll m)//卢卡斯定理
{
return a && b ? (lucas(a/m,b/m,m) % m * comp(a%m,b%m,m) ) % m : 1;
}
int main()
{
ll a,b,m;
cin>>b>>a>>m;
cout<<lucas(a,b,m)<<endl;
}