【算法】扩展卢卡斯详解

前置芝士

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

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

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

问题形式

卢卡斯( L u c a s Lucas Lucas)和扩展卢卡斯( e x L u c a s exLucas exLucas)都用于求解形如 C n m m o d    p C_{n}^{m} \mod p Cnmmodp的答案。当 p p 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=1npiai
那么分解完后每一项 p i a i ​ p_i^{a_i}​ piai之间两两互质,只要我们能得出每组 C n m m o d    p i a i ​ C_n^m \mod p_i^{a_i}​ Cnmmodpiai的答案,就可以用中国剩余定理合并得到最后的答案。

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

我们从 n ! ​ n!​ n!中把 p ​ 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 n!=ppn×pn!×pini
式子中的 p ⌊ n p ⌋ p^{\lfloor\frac{n}{p}\rfloor} ppn可以直接快速幂求出, ⌊ n p ⌋ ! \lfloor \frac{n}{p} \rfloor ! pn!考虑递归求解,唯一比较麻烦的是后面剩下的不能被 p p p整除的数。

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

举个经典的例子: n = 19 n=19 n=19 p = 3 p=3 p=3 a = 2 ​ a=2​ a=2
n ! = 1 ∗ 2 ∗ 3 ∗ 4 ∗ 5 ∗ 6 ∗ 7 ∗ 8 ∗ 9 ∗ 10 ∗ 11 ∗ 12 ∗ 13 ∗ 14 ∗ 15 ∗ 16 ∗ 17 ∗ 18 ∗ 19 = ( 1 ∗ 2 ∗ 4 ∗ 5 ∗ 7 ∗ 8 ) ∗ ( 10 ∗ 11 ∗ 13 ∗ 14 ∗ 16 ∗ 17 ) ∗ 19 ∗ 3 6 ∗ 6 ! ≡ ( 1 ∗ 2 ∗ 4 ∗ 5 ∗ 7 ∗ 8 ) 2 ∗ 19 ∗ 3 6 ∗ 6 ! \begin{aligned} n! &= 1*2*3*4*5*6*7*8*9*10*11*12*13*14*15*16*17*18*19 \\ &=(1*2*4*5*7*8)*(10*11*13*14*16*17)*19*3^6*6! \\ & \equiv(1*2*4*5*7*8)^2*19*3^6*6! \end{aligned} n!=12345678910111213141516171819=(124578)(101113141617)19366!(124578)219366!
于是我们对括号内的求一遍,然后快速幂,接着暴力求未能构成循环节的 19 19 19,最后递归求解 6 ! 6! 6!

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

这一部分的代码:

为了方便,我们会把 p a p^a pa当做参数传入到 f a c fac fac函数中,即代码中的 k 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​ px的项,那么具体的操作就是:我们用一个 c n t ​ cnt​ cnt变量记录最后的式子中有多少个因子 p ​ p​ p,那么 c n t ​ cnt​ cnt就等于 n ​ n​ n的阶乘中含有因子 p ​ p​ p的数量减去 m ​ m​ m n − m ​ n-m​ nm的阶乘中含有的 p ​ p​ p的数量。

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

//记得开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} n!inv(m!)inv((nm)!)pcnt

这一段的代码:

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 exLucas部分。

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

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

先放 c r t crt crt代码:

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

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

整段 e x L u c a s exLucas 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 
#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;
}

你可能感兴趣的:(模板,数论,卢卡斯定理)