扩展 Lucas 定理学习笔记 & 详解,一文带你彻底看懂扩展 Lucas 定理。

背景

考虑这样一个问题:

给定 \(n, r, p\) ,求

\\\binom{n}{r} \\bmod m \\

不保证 \(m\) 是质数。\(1\le r\le n\le 10^{18}\)。

显然此时普通 Lucas 定理就无能为力了。这时需要用到扩展 Lucas 定理(exLucas)。

前置知识

你需要知道:

约定记号

  • \(v_p(n!)\) :\(n!\) 中质因子 \(p\) 的次数;

  • \((n!)_p\) :\(n!\) 去掉所有的 \(p\) 因子。

算法流程

由于 \(m\) 不是质数,我们考虑以下的这三个步骤。它们组成了扩展 Lucas 的思路框架。

  1. 我们将模数 \(m\) 分解质因数:\(m=p_1^{a_1}p_2^{a_2}\cdots p_{n}^{a_n}\) ;

  2. 我们构造方程组:

    \\\begin{cases} x \\equiv r_1 \\pmod{p_1\^{a_1}} \\\\ x \\equiv r_2 \\pmod{p_2\^{a_2}} \\\\ \\cdots \\cdots\\\\ x \\equiv r_n \\pmod{p_n\^{a_n}} \\end{cases} \\

  3. 注意到这些方程里的模数两两互质。我们使用 CRT 合并求解 \(x\) ,即为我们想要的答案 \(\binom{n}{r}\bmod m\) 。

接下来的问题就完全聚焦到了 \(r_i\) 的构造上来。我们考虑如下流程:

  1. 明确我们要构造 \(r_i\equiv \binom{n}{r}\pmod{p_i^{a_i}}\) ;

  2. 转化问题。我们需要求解 \(\binom{n}{r}\bmod p^a\) ,其中 \(p\) 是质数。即我们要求解如下式子:

    \\\frac{n!}{n!(n-r)!}\\bmod p\^a \\

  3. 注意到分母 \(r!(n-r)!\) 与 \(p^a\) 不一定互质。那么就意味着我们没有办法求分母的逆元,进一步我们无法求解这个分式。考虑我们该如何改写它。

  4. 我们目标是使分母和 \(p^a\) 互质。为此我们将式子改写为:

    \p\^v\\frac{(n!)_p}{(r!)_p((n-r)!)_p} \\bmod p\^a \\

    其中 \(v=v_p(n!)-v_p(r!)-v_p((n-r)!)\) 。注意到若 \(v\ge a\) ,则结果为 \(0\) 。

  5. 计算 \(v_p(n!)\) 。这可以利用 Legendre 公式求出:

    \v_p(n!) = \\sum_{i=1}\^{\\infty}\\left\\lfloor \\frac{n}{p\^i} \\right\\rfloor \\

  6. 计算 \((n!)_p\) 。利用前置知识中提到的递归公式:

    \(n!)_p \\equiv (\\pm1)\^{\\lfloor n/p\^a\\rfloor}(\\lfloor n/p \\rfloor!)_p \\left(\\prod_{1\\le j\\le(n\\bmod p\^a)且j\\nmid p}j\\right) \\pmod{p\^a} \\

    一些注意事项:

    i. \((\lfloor n/p\rfloor!)_p\) 可以利用循环来展开递归,加快速度;

    ii. \(\prod j\) 项,等价于在 \((n\bmod p^a)!\) 中去掉 \(p\) 因子,故这里可以直接预打表,用 f[i] 来表示 \(i!\) 中去掉 \(p\) 因子的值。这个表只用打到 \(i\lt p^a\) 即可。

    iii. 式子中 \(\pm 1\) 的取值:当且仅当 \(p=2\) 且 \(a\ge 3\) 时,\(\pm1\) 取正;否则取负。

于是我们完成了一个 \(r_i\) 的构造。按此流程,我们对每一个 \(p_i^{a_i}\) 构造一个 \(r_i\) ,就完成了方程组的构造。

复杂度分析

  1. 我们必须分解 \(m\) 。用试除法分解质因数的复杂度是 \(\mathcal O(\sqrt{m})\) 。
  2. 我们必须处理每个质因子。其中 Legendre 公式 和 递归公式 的迭代次数都是 \(\mathcal O(\log_pn)\) 。

综上,这个算法的复杂度为 \(\mathcal O(\sqrt{m}+\sum\log_{p_i}n)\) 。

模板代码

这就是对上文思路的完全再现。如果还有不理解的过程,请参见下面的代码。

cpp 复制代码
#include<iostream>
#include<vector>
#define fastio ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define int long long
#define MIKU 0
using namespace std;

int n, m, p;  //求解 C(n,m) mod p。

//扩展欧几里得求逆元。
int exgcd(int a, int b, int &x, int &y) {
	if(b == 0) {x = 1; y = 0; return a;}
	int d = exgcd(b, a%b, y, x);
	y -= a / b * x;
	return d;
}

int inv(int a, int m) {
	int x, y;
	exgcd(a, m, x, y);
	return (x % m + m) % m;
}

//对每个质因数构造 r_i
struct BPP {
	int p, a, pa;  //p:模数  a:次数  pa:p^a。
	vector<int> f;  //预处理 f[i] ,表示 i! 去掉 p 因子。
    //构造函数,计算出 pa 和 f。
	BPP(int prime, int power) : p(prime), a(power) {
		pa = 1;
		for(int i=1; i<=a; i++) pa *= p;
		f.resize(pa); f[0] = 1;
		for(int i=1; i<pa; i++) f[i] = (i % p) ? f[i-1] * i % pa : f[i-1];
	}
    //利用 Legendre 公式求 vp(n!)
	int vp(int n, int cnt = 0) {
		for(; n; n/=p) cnt += n / p;
		return cnt;
	}
    //利用递归公式求 (n!)_p 。代码中用循环代替递归。
	int fac(int n, int res = 1) {
		bool neg = (p != 2 || a < 3);
		for(; n>1; n/=p) {
			if((n / pa) & neg) res = pa - res;  //这一步完成符号修正,即公式中第一个因子。
			res = res * f[n%pa] % pa;
		}
		return res;
	}
    //利用 vp(n!) 和 (n!)_p 算 C(n,r) mod p^a。 
	int C(int n, int r) {
		int v = vp(n) - vp(r) - vp(n-r);
		if(v >= a) return 0;
		int res = fac(n) * inv(fac(r) * fac(n-r) % pa, pa) % pa;
		for(; v; v--) res = res * p % pa;
		return res;
	}
};

//扩展 Lucas ,负责完成质因数分解和 CRT 合并。
int exLucas(int n, int r, int m, int res = 0) {
	int tmp = m;
	for(int p=2; p*p<=tmp; p++) {  //试除法分解质因数。
		if(tmp % p) continue;
		int a = 0, pa = 1;
		for(; tmp%p==0; tmp/=p) a ++, pa *= p;
		BPP bpp = BPP(p, a);
		int ri = bpp.C(n, r);
		int mi = m / pa;
		res = (res + mi * ri % m * inv(mi, pa)) % m;  // CRT 合并公式。
	}
	if(tmp > 1) {
		BPP bpp = BPP(tmp, 1);
		int ri = bpp.C(n, r);
		int mi = m / tmp;
		res = (res + mi * ri % m * inv(mi, tmp)) % m;
	}
	return res;
}

signed main() {
	fastio;
	cin>>n>>m>>p;
	cout<<exLucas(n, m, p);
	return 0;
}

例题

洛谷 P4720 【模板】扩展卢卡斯定理 / exLucas

这是一道模板题。代码见上面的模板代码。


洛谷 P2480 SDOI2010\ 古代猪文

这题其实并没有用到扩展 Lucas,但是它的思路和扩展 Lucas 是相通的。

详细题解戳 这里


洛谷 P2183 国家集训队 礼物

我们对 \(w_i\) 做前缀和,显然答案是 \(\sum_{i=1}^m\binom{n-s_{i-1}}{w_i}\) 。

容易注意到当 \(s_{m}>n\) 是无解。

主要难点是不保证模数为质数。这就需要用到扩展 Lucas 定理。

下面贴出代码。相比于上面的模板代码,只改动了主函数。
Show Me the Code

cpp 复制代码
#include<iostream>
#include<vector>
#define fastio ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define int long long
#define MIKU 0
using namespace std;

int mod, n, m, ans = 1;
int w[10], s[10];

int exgcd(int a, int b, int &x, int &y) {
	if(b == 0) {x = 1; y = 0; return a;}
	int d = exgcd(b, a%b, y, x);
	y -= a / b * x;
	return d;
}

int inv(int a, int m) {
	int x, y;
	exgcd(a, m, x, y);
	return (x % m + m) % m;
}

struct BPP {
	int p, a, pa;
	vector<int> f;
	BPP(int prime, int power) : p(prime), a(power) {
		pa = 1;
		for(int i=1; i<=a; i++) pa *= p;
		f.resize(pa); f[0] = 1;
		for(int i=1; i<pa; i++) f[i] = (i % p) ? f[i-1] * i % pa : f[i-1];
	}
	int vp(int n, int cnt = 0) {
		for(; n; n/=p) cnt += n / p;
		return cnt;
	}
	int fac(int n, int res = 1) {
		bool neg = (p != 2 || a < 3);
		for(; n>1; n/=p) {
			if((n / pa) & neg) res = pa - res;
			res = res * f[n%pa] % pa;
		}
		return res;
	}
	int C(int n, int r) {
		int v = vp(n) - vp(r) - vp(n-r);
		if(v >= a) return 0;
		int res = fac(n) * inv(fac(r) * fac(n-r) % pa, pa) % pa;
		for(; v; v--) res = res * p % pa;
		return res;
	}
};

int exLucas(int n, int r, int m, int res = 0) {
	int tmp = m;
	for(int p=2; p*p<=tmp; p++) {
		if(tmp % p) continue;
		int a = 0, pa = 1;
		for(; tmp%p==0; tmp/=p) a ++, pa *= p;
		BPP bpp = BPP(p, a);
		int ri = bpp.C(n, r);
		int mi = m / pa;
		res = (res + mi * ri % m * inv(mi, pa)) % m;
	}
	if(tmp > 1) {
		BPP bpp = BPP(tmp, 1);
		int ri = bpp.C(n, r);
		int mi = m / tmp;
		res = (res + mi * ri % m * inv(mi, tmp)) % m;
	}
	return res;
}

signed main() {
	fastio;
	cin>>mod>>n>>m;
	for(int i=1; i<=m; i++) cin>>w[i], s[i] = s[i-1] + w[i];
	if(s[m] > n) {cout<<"Impossible"; return MIKU;}
	for(int i=1; i<=m; i++) ans = ans * exLucas(n-s[i-1], w[i], mod) % mod;
	cout<<ans;
	return MIKU;
}

后记

虽然扩展 Lucas 看着很复杂,但实际上也就只是一个求组合数的工具。真正的难题,其难点往往也不止在扩展 Lucas 。更重要的是领会扩展 Lucas 背后的数学原理和思想方法。