卢卡斯定理求组合数(逆元+费马小定理+扩展欧几里得)

本文探讨了一道鸽子排班问题,利用卢卡斯定理解决组合数求余问题,介绍了乘法逆元和费马小定理的概念及应用,最后给出了求解组合数的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天在刷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<bits/stdc++.h>
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;
}

此时算法就写完了,交一发
在这里插入图片描述
完美AC,开心的去吃饭

<think>嗯,用户想要了解费马小定理逆的原理以及对应的C++代码模板。首先,我需要回忆一下费马小定理的具体内容。费马小定理是说,如果p是一个质数,而a不是p的倍数,那么a的(p-1)次方模p等于1。数学表达式就是a^(p-1) ≡ 1 mod p。这样的话,如果我们想a在模p下的逆元,也就是一个数x,使得a*x ≡1 mod p,可以利用这个定理。 那如何用费马小定理逆元呢?因为根据定理,a^(p-1) ≡1 mod p,所以两边同时乘以a的逆元,应该能得到a^(p-2) ≡x mod p。所以逆元x就是a的(p-2)次方模p的结果。不过这里的前提是p必须是质数,而且a和p互质,也就是a不是p的倍数。这点需要特别注意,如果p不是质数,就不能用费马小定理逆元了,可能需要用扩展欧几里得算法。 接下来是C++代码部分。通常快速幂可以用递归或者迭代的方法。这里可能需要一个快速幂函数来计算a的(p-2)次方mod p。比如,写一个函数叫做pow_mod,参数是a、b(指数)、mod,返回a^b % mod的值。然后,在逆元的时候,调用这个函数,传入a,p-2,p。需要注意的是,如果p不是质数,或者a和p不互质,这种情况下逆元会出错,所以代码里可能需要做一些错误处理。 那用户可能的应用场景是什么呢?可能是在做算法题的时候需要计算模逆元,比如组合数计算或者模运算下的除法。这时候费马小定理提供了一种快速解的方法,但必须保证模数是质数。比如常见的模数1e9+7就是一个质数,这时候用费马小定理比较方便。 另外,要提醒用户,当p不是质数时,这个方法不适用,这时候应该使用扩展欧几里得算法逆元。所以在代码里可能需要根据情况选择不同的方法,但用户现在的问题可能只需要费马小定理的情况。 现在考虑代码模板的结构。首先快速幂的实现,迭代法可能效率更高,避免递归的栈开销。快速幂函数的大致结构是初始化结果为1,然后循环指数的每一位,如果当前位是1,就乘上当前的a的幂,同时每次a自乘并取模。 然后,逆元函数就调用快速幂,计算a^(p-2) mod p。同时,在调用之前需要检查p是否为质数,但可能在模板中这部分检查会被省略,因为判断质数本身也需要时间,尤其是当p很大时。所以用户需要自己确保p是质数,否则结果不正确。 可能的测试例子,比如p=7,a=3,那么逆元应该是5,因为3*5=15≡1 mod7。用代码计算的话,3^(7-2)=3^5=243,243 mod7是多少?243/7=34*7=238,余5,所以正确。 所以代码的大致结构应该是: #include <iostream> using namespace std; long long pow_mod(long long a, long long b, long long mod) { long long res = 1; while (b > 0) { if (b % 2 == 1) { res = (res * a) % mod; } a = (a * a) % mod; b /= 2; } return res; } long long inv_mod(long long a, long long p) { return pow_mod(a, p-2, p); } int main() { long long a = 3, p = 7; cout << inv_mod(a, p) << endl; // 应该输出5 return 0; } 这里需要注意数据类型的范围,如果mod很大,比如1e18的话,可能需要使用更大的类型,但通常用long long足够了。另外,当a和p不互质时,这个函数会返回错误的结果,所以用户需要确保输入的a和p满足条件。 有没有可能的情况,即使p是质数,但a是p的倍数?此时a mod p等于0,所以逆元是不可能的,代码返回0,但实际这种情况应该报错。所以在实际应用中可能需要添加条件判断,比如如果a%p ==0,则返回无解。但可能在模板中这部分没有处理,用户需要自己注意。 所以,总结回答的时候需要说明原理是基于费马小定理,将逆元转化为a^(p-2) mod p,并给出代码示例,同时强调p必须是质数且a和p互质,否则方法不适用。</think>### 费马小定理逆元原理 **费马小定理**指出:若 \( p \) 为质数,且 \( a \) 与 \( p \) 互质(即 \( a \nmid p \)),则: \[ a^{p-1} \equiv 1 \ (\text{mod} \ p) \] 将两边同乘以 \( a^{-1} \)(模 \( p \)逆元),得到: \[ a^{p-2} \equiv a^{-1} \ (\text{mod} \ p) \] 因此,逆元可通过计算 \( a^{p-2} \ \text{mod} \ p \) 得到。 **前提条件**: - \( p \) 必须为质数。 - \( a \) 和 \( p \) 必须互质(即 \( a \neq kp \))。 --- ### C++ 代码模板 #### 快速幂取模函数 ```cpp #include <iostream> using namespace std; // 快速幂取模:计算 (a^b) % mod long long pow_mod(long long a, long long b, long long mod) { long long res = 1; while (b > 0) { if (b % 2 == 1) // 等价于 b & 1 res = (res * a) % mod; a = (a * a) % mod; b /= 2; // 等价于 b >>= 1 } return res; } // 费马小定理逆元:返回 a 在模 p 下的逆元 long long inv_mod(long long a, long long p) { return pow_mod(a, p - 2, p); } int main() { long long a = 3, p = 7; // 示例输入 cout << "逆元:" << inv_mod(a, p) << endl; // 输出 5 return 0; } ``` --- ### 注意事项 1. **错误处理**:若 \( p \) 非质数或 \( a \) 是 \( p \) 的倍数,结果将不正确。需额外检查输入合法性。 2. **时间复杂度**:快速幂的时间复杂度为 \( O(\log p) \),效率极高。 3. **替代方法**:当 \( p \) 非质数时,需改用扩展欧几里得算法逆元。 --- ### 示例验证 - 输入 \( a=3, p=7 \),计算 \( 3^{7-2} \mod 7 = 3^5 \mod 7 = 243 \mod 7 = 5 \)。 - 验证:\( 3 \times 5 = 15 \equiv 1 \ (\text{mod} \ 7) \),结果正确。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值