【算法】扩展卢卡斯详解

版权声明:Powered By Fighter https://blog.csdn.net/qq_30115697/article/details/88942177

前置芝士

扩展卢卡斯相对较为复杂,需要较多的前置芝士。

  1. 快速幂
  2. 质因数分解
  3. 组合数公式
  4. 扩展欧几里得(exgcd)求逆元
  5. 中国剩余定理(或excrt)
  6. 熟练阅读Latex

至于卢卡斯定理,那真的不重要。

问题形式

卢卡斯( L u c a s Lucas )和扩展卢卡斯( e x L u c a s exLucas )都用于求解形如 C n m m o d    p C_{n}^{m} \mod p 的答案。当 p p 是质数时,直接用卢卡斯定理就可以*过去。

如果 p p​ 不是质数,就得用今天的主角——扩展卢卡斯定理求解。

求解思想

由于 p p 不是质数,那么我们考虑强行对其进行质因数分解:
p = i = 1 n p i a i p=\prod_{i=1}^{n}p_i^{a_i}
那么分解完后每一项 p i a i p_i^{a_i}​ 之间两两互质,只要我们能得出每组 C n m m o d    p i a i C_n^m \mod p_i^{a_i}​ 的答案,就可以用中国剩余定理合并得到最后的答案。

然后来考虑求解 C n m m o d    p i a i C_n^m \mod p_i^{a_i} ,我们知道 C n m = n ! m ! × ( n m ) ! C_n^{m}=\frac{n!}{m!\times (n-m)!}​ ,那么我们也就是要求下面这个式子:
n ! m ! × ( n m ) ! m o d    p a \frac{n!}{m!\times (n-m)!}\mod p^{a}
那么显然我们要求出阶乘和阶乘的逆元。由于阶乘与模数当前不互质,所以极有可能不存在直接的逆元,需要我们对式子进行进一步的拆分。

我们从 n ! n!​ 中把 p p​ 的倍数全部提出,原式就会变成这样一个式子:
n ! = p n p × n p ! × p i n i n!=p^{\lfloor\frac{n}{p}\rfloor}\times \lfloor\frac{n}{p}\rfloor!\times \prod_{p \nmid i}^{n}i
式子中的 p n p p^{\lfloor\frac{n}{p}\rfloor} 可以直接快速幂求出, n p ! \lfloor \frac{n}{p} \rfloor ! 考虑递归求解,唯一比较麻烦的是后面剩下的不能被 p p 整除的数。

仔细观察~~(去你丫的)~~可以发现,剩下这些项的乘积有循环节,长度小于 p a p^a​ 。由于显然 x x + p a ( m o d p a ) x \equiv x+p^a \pmod {p^a}​ ,所以可以求出共有几个循环节,对第一个循环节暴力求一遍,然后用快速幂搞定。对于最后剩余的不能构成完整循环节的几项,依然是暴力求一遍搞定。

举个经典的例子: n = 19 n=19 p = 3 p=3 a = 2 a=2​
KaTeX parse error: No such environment: align at position 8: \begin{̲a̲l̲i̲g̲n̲}̲ n! &= 1*2*3*4*…
于是我们对括号内的求一遍,然后快速幂,接着暴力求未能构成循环节的 19 19 ,最后递归求解 6 ! 6!

由于 3 6 3^6 次这一项比较特殊,因为存在这一项,所以才可能不存在逆元,所以求阶乘的时候我们不对其进行计算,而在求组合数的时候单独处理。也就是说,我们求的阶乘,实际上是去掉了所有因子 p p 后的阶乘。

这一部分的代码:

为了方便,我们会把 p a p^a 当做参数传入到 f a c fac 函数中,即代码中的 k k

ll fac(ll n, ll p, ll k) {      //n! % k (k=p^a)
    if(!n) return 1;		//0! = 1
    ll ans = 1;
    for(int i = 2; i <= k; i++) {		//处理循环节内的数字
        if(i%p) ans = ans*i % k;
    }
    ans = qpow(ans, n/k, k);		//所有循环节的乘积
    for(int i = 2; i <= n%k; i++) {		//剩下单独的项
        if(i%p) ans = ans*i % k;
    }
    return ans*fac(n/p, p, k)%k;		//递归求解
}

再回到求解组合数的过程。

上面说到,要在求组合数时单独处理形如 p x p^x​ 的项,那么具体的操作就是:我们用一个 c n t cnt​ 变量记录最后的式子中有多少个因子 p p​ ,那么 c n t cnt​ 就等于 n n​ 的阶乘中含有因子 p p​ 的数量减去 m m​ n m n-m​ 的阶乘中含有的 p p​ 的数量。

求法就是不断枚举范围内有多少个 p x p^x 的倍数(小学基本功)。

//记得开long long,n很大,直接把int炸飞
for(ll i = p; i <= n; i *= p) cnt += n/i;
for(ll i = p; i <= m; i *= p) cnt -= m/i;
for(ll i = p; i <= n-m; i *= p) cnt -= (n-m)/i;;

然后答案就是 n ! i n v ( m ! ) i n v ( ( n m ) ! ) p c n t n!*inv(m!)*inv((n-m)!)*p^{cnt}

这一段的代码:

ll C(ll n, ll m, ll p, ll k){	//k = p^a
   	if(n < m) return 0;
	ll a = fac(n,p,k), b = fac(m,p,k), c = fac(n-m,p,k);
	ll cnt = 0;
	for(ll i = p; i <= n; i *= p) cnt += n/i;
	for(ll i = p; i <= m; i *= p) cnt -= m/i;
	for(ll i = p; i <= n-m; i *= p) cnt -= (n-m)/i;
	return a*inv(b, k)%k * inv(c, k)%k * qpow(p, cnt, k)%k;
}

接下来就到了正式的 e x L u c a s exLucas 部分。

首先我们把给定的模数 p p 进行质因数分解。

然后对于每个 p i a i p_i^{a_i} ,都求一遍组合数 C n m m o d &ThinSpace;&ThinSpace; p i a i C_n^m \mod p_i^{a_i} ,然后同时用 c r t crt 合并。

先放 c r t crt 代码:

ll crt(ll n, ll mod){
	return n*(p/mod)%p*inv(p/mod, mod)%p;
}

p p 是给定的模数,就相当于 c r t crt 中所有模数的乘积, m o d mod 是当前的质因数分解出来的模数,就相当于单个方程的模数。

整段 e x L u c a s exLucas 代码:

ll exlucas(){
	ll t = p, ans = 0;
	for(ll i = 2; i*i <= t; i++){	//质因数分解
		if(t%i) continue;
		ll tmp = 1;
		while(t%i == 0){		//求出pi^ai
			tmp *= i;
			t /= i;
		}
		ans = (ans+crt(C(n, m, i, tmp), tmp))%p;	//crt合并
	}
	if(t > 1) ans = (ans+crt(C(n, m, t, t), t))%p;	//如果剩下的t是大质数,再进行一次计算
	return ans%p;
}

完整代码

洛谷P4720 【模板】扩展卢卡斯

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

ll n, m, p;

ll exgcd(ll a, ll b, ll &x, ll &y){
	if(!b){
		x = 1, y = 0;
		return a;
	}
	ll res = exgcd(b, a%b, x, y);
	ll t = x;
	x = y;
	y = t-a/b*y;
	return res;
}

ll qpow(ll a, ll n, ll mod){
	ll res = 1;
	while(n){
		if(n&1) res = (res*a) % mod;
		a = (a*a) % mod;
		n >>= 1;
	}
	return res;
}

ll inv(ll a, ll p){
	ll x, y;
	exgcd(a, p, x, y);
	if(x+p > p) return x;
	return x+p;
}

ll crt(ll n, ll mod){
	return n*(p/mod)%p*inv(p/mod, mod)%p;
}

ll fac(ll n, ll p, ll k){		//k = p^x
   	if(!n) return 1;
   	ll ans = 1;
	for(int i = 2; i <= k; i++){
		if(i%p) ans = ans*i % k;
	}
	ans = qpow(ans, n/k, k);
	for(int i = 2; i <= n%k; i++){
		if(i%p) ans = ans*i % k;
	}
	return ans*fac(n/p, p, k)%k;
}

ll C(ll n, ll m, ll p, ll k){	//k = p^x
   	if(n < m) return 0;
	ll a = fac(n,p,k), b = fac(m,p,k), c = fac(n-m,p,k);
	ll cnt = 0;
	for(ll i = p; i <= n; i *= p) cnt += n/i;
	for(ll i = p; i <= m; i *= p) cnt -= m/i;
	for(ll i = p; i <= n-m; i *= p) cnt -= (n-m)/i;
	return a*inv(b, k)%k * inv(c, k)%k * qpow(p, cnt, k)%k;
}

ll exlucas(){
	ll t = p, ans = 0;
	for(ll i = 2; i*i <= t; i++){
		if(t%i) continue;
		ll tmp = 1;
		while(t%i == 0){
			tmp *= i;
			t /= i;
		}
		ans = (ans+crt(C(n, m, i, tmp), tmp))%p;
	}
	if(t > 1) ans = (ans+crt(C(n, m, t, t), t))%p;
	return ans%p;
}

int main()
{
	cin >> n >> m >> p;
	cout << exlucas() << endl;
	
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_30115697/article/details/88942177