浅谈 Lucas 定理
说明
由于篇幅原因,大部分例题不放代码。
【组合数学】上一篇:浅谈排列组合。
Part 0:前置知识
- 组合数。
Part 1:卢卡斯定理是什么
在算法竞赛中,我们经常需要计算组合数
按照三种常规方法均有局限性:
- 直接计算法:
\binom nk = \frac{n!}{k!(n - k)!} ,当n 很大时(例如超过10^8 )阶乘将会难以计算并存储; - 递推法:
\binom nk = \binom {n - 1}{k - 1} + \binom {n - 1}k ,时间复杂度\mathcal{O}(nk) ,这种方法仅支持小的n, k ; - 预处理阶乘+逆元法:这种方法可以处理
n < p 的情况,但当n \ge p 时,n! 中包含因子p ,导致n! \bmod p = 0 ,无法求逆元。
但卢卡斯定理专门解决大组合数对小质数取模的问题。当
Part 2:卢卡斯定理的公式
2.1:数学常用写法
设
其中
则有:
特别地,当
2.2:竞赛常用写法
在竞赛中,我们常使用递归形式:
Part 3:卢卡斯定理的证明
3.1:关键引理
若
证明:
展开组合数,得:
又
3.2:二项式展开的模性质
对于任意整数
证明:
根据二项式定理:
那么:
根据引理,当
所以:
3.3:推广形式
对于任意非负整数
证明:
使用数学归纳法。
当
假设当
那么,当
成立。
3.4:卢卡斯定理的证明
将
考虑二项式展开
根据推论:
所以:
比较等式两边
- 左边:根据二项式定理,
x^k 的系数为\binom nk ; - 右边:从每一个因子
(1 + x^{p^i})^{n_i} 中选择x^{k_ip^i} 项,而这一项的系数为\binom {n_i}{k_i} 。因为k = \displaystyle\sum_{i = 0}^mk_ip^i ,所以x^k 的系数为\displaystyle \prod_{i = 0}^m \binom {n_i}{k_i} 。
由于等式两边
证毕。
Part 4:代码实现
- 预处理阶乘(也可以有逆元);
- 写好组合数计算函数;
- 将
n 和k 不断除以p ,然后递归实现 Lucas 函数; - 记得特判
n < k 和k = 0 的情况。
可以先自己尝试写,然后再参考示例代码。
::::success[Code]
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 2e5 + 20;
ll n, m, p, fac[kMaxN];
ll Qpow(ll a, ll b, ll p) {
ll ans = 1;
for (; b; b >>= 1) {
if (b & 1) {
ans = ans * a % p;
}
a = a * a % p;
}
return ans;
}
ll C(ll n, ll m, ll p) {
if (n < m) {
return 0;
}
return fac[n] * Qpow(fac[m], p - 2, p) % p * Qpow(fac[n - m], p - 2, p) % p;
}
ll Lucas(ll n, ll m, ll p) {
if (n < m) {
return 0;
} else if (!n) {
return 1;
}
return Lucas(n / p, m / p, p) * C(n % p, m % p, p) % p;
}
void Init() {
fac[0] = 1;
for (int i = 1; i < kMaxN; ++ i) {
fac[i] = fac[i - 1] * i % p;
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t;
for (cin >> t; t; -- t) {
cin >> n >> m >> p, Init();
cout << Lucas(n + m, n, p) << '\n';
}
return 0;
}
::::
Part 5:例题
P2606 [ZJOI2010] 排列计数
题目中的条件完全二叉树小根堆性质,问题转化为统计合法小根堆构造方案数。
定义
小根堆根节点必为子树最小值,剩余
从叶子向上递推计算,空树边界
组合数需结合卢卡斯定理计算。
::::success[Code]
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 3e6 + 10;
ll n, p, fac[kMaxN], f[kMaxN], sz[kMaxN], ans;
ll Qpow(ll a, ll b, ll p) {
ll ans = 1;
for (; b; b >>= 1) {
if (b & 1) {
ans = ans * a % p;
}
a = a * a % p;
}
return ans;
}
ll C(ll n, ll m, ll p) {
if (n < m) {
return 1;
}
return fac[n] * Qpow(fac[m], p - 2, p) % p * Qpow(fac[n - m], p - 2, p) % p;
}
ll Lucas(ll n, ll m, ll p) {
if (n < m) {
return 1;
} else if (!n) {
return 1;
}
return Lucas(n / p, m / p, p) * C(n % p, m % p, p) % p;
}
void Init() {
fac[0] = 1;
for (int i = 1; i < kMaxN; ++ i) {
fac[i] = fac[i - 1] * i % p;
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> p, Init();
fill(sz + 1, sz + n + 1, 1), fill(f + n + 1, f + (n << 1) + 2, 1);
for (int i = n; i >= 2; -- i) {
sz[i >> 1] += sz[i];
}
for (int i = n; i; -- i) {
f[i] = Lucas(sz[i] - 1, sz[i << 1], p) * f[i << 1] % p * f[(i << 1) | 1] % p;
}
cout << f[1] << '\n';
return 0;
}
::::
P7976 「Stoi2033」园游会
先转化函数:
简单来说,就是把每个组合数对
数据范围很大,无法直接计算组合数,因此使用卢卡斯定理处理模
Lucas 定理表明:将
若存在某一位
逐位分析三进制每一位的总贡献:
- 当
n_i = 0 时,m_i 只能取0 ,总贡献为1 。 - 当
n_i = 1 时,m_i 可取0, 1 ,两种情况贡献均为1 ,总贡献为2 。 - 当
n_i = 2 时,m_i 可取0, 1, 2 ,对应贡献为1, -1, 1 ,总贡献为1 。
可以发现,仅当三进制位
设这样的位数为
由于三进制每一位独立,答案就是所有位贡献相乘:
遍历