数论小桶
【前言】
由两道题引领的数论小全家桶(雾)。
显然一篇文章无法涵盖数论全部内容,本文大概涵盖的内容有:
- 欧拉定理 & 费马小定理。
- Bézout 定理 & 扩展欧几里得算法。
- 线性同余方程 & 乘法逆元。
- 中国剩余定理 & 扩展。
- 卢卡斯定理。
本文将逐一简略介绍,且基本不涉及定理的证明。
【前置芝士】
【欧拉定理 & 费马小定理】
费马小定理:
若 \(p\) 为质数,\(\gcd(a,p)=1\),则 \(a^{p-1}\equiv 1\pmod p\)
同时对于任意整数 \(a\),有 \(a^p\equiv a\pmod p\)
欧拉定理:
若 \(\gcd(a,p)=1\),则有 \(a^{\phi(p)}=1\pmod p\)
扩展欧拉定理:
\(a^b~\equiv~\begin{cases}a^{b~Mod~\phi(p)} &\gcd(a,p)~=~1 \\a^b &\gcd(a,p)~\neq~1~\land~b~<~\phi(p) \\ a^{b~Mod~\phi(p)~+~\phi(p)} &\gcd(a,p)~\neq~1~\land~b~\geq~\phi(p)\end{cases}\pmod p\)
应用:
- 利用费马小定理,可以快速求出模数 \(p\) 为质数时,某个数 \(a\) 的乘法逆元为 \(a^{p-2}\)。
- 利用扩展欧拉定理,可以在幂的指数很大时,快速缩小数据范围。
注意:费马小定理使用的前提是 \(\gcd(a,p)=1\)。
【Bézout 定理 & 扩展欧几里得算法】
Bézout 定理:
设 \(a,b\) 均为整数且 \(a\times b\neq 0\),那么一定存在整数 \(x,y\) ,使得 \(ax+by=\gcd(a,b)\)
证明用到欧几里得算法(辗转相除法)求最小公倍数,同时还得出了扩展欧几里得算法:
LL Exgcd(LL a, LL b, LL &x, LL &y){
if(!b) {x=1, y=0; return a;}
LL d = Exgcd(b, a % b, x, y);
LL z = x; x = y; y = z - y * (a / b);
return d;
}
应用:Bézout 定理是扩展欧几里得算法的理论依据,而后者的运用十分广泛。(见后)
【线性同余方程 & 乘法逆元】
求解线性同余方程 \(ax\equiv b\pmod p\)。
相当于求解方程 \(ax+py=b\)。
不难利用 exgcd 求出 \(ax+py=\gcd(a,p)\) 的一个解 \(x_0\)。
那么原同余方程的一个解就是 \(x_1=x_0/\gcd(a,p)\times b\)。
原方程的通解就是与 \(\forall x,x\equiv x_1 \pmod {p/\gcd(a,p)}\)。
求整数 \(a\) 在 \(\mod p\) 意义下的乘法逆元。
- 若 \(p\) 为质数且 \(\gcd(a,p)=1\),\({\rm inv}(a)=a^{p-2} \pmod p\)
- 对于更一般的情况直接求解线性同余方程 \(a\times x\equiv 1\pmod p\)。
不难发现,单次求逆元的复杂度为 \(O(\log n)\),但或许我们还需要一次性求出 \(n\) 个数的逆元。(栗子)
那么我们可以试着推一下方程:
求出 \(1\sim n\) 在 \({\rm mod\ p}\) 意义下的逆元。
首先有 \(1^{-1}\equiv 1\pmod p\)。
对于某个数 \(i\),设 \(p=k\times i+r\),那么显然:
\[k\times i+r\equiv 0\pmod p \]\[k\times r^{-1}+i^{-1}\equiv 0\pmod p \]\[i^{-1}\equiv -k\times r^{-1}\pmod p \]\[i^{-1}\equiv -\lfloor\frac{p}{i}\rfloor\times (p\ {\rm mod}\ i)^{-1} \]
不难写出代码:
inv[1] = 1;
for(int i=2; i<=n; i++)
inv[i] = 1LL * (p - p / i) * inv[p % i] % p;
还有一个线性求阶乘逆元的方法:(根据逆元的定义即可简单推出)
inv[n] = Pow(fac[n], MOD - 2);
for(int i=n-1; i>=1; i--)
inv[i] = inv[i + 1] * (i + 1) % MOD;
它们本身就是上述算法的运用。
【中国剩余定理 & 扩展】
求解同余方程:
保证所有 \(m_i\) 两两互质。
中国剩余定理:
\(M=\Pi_{i=1}^k m_i\),\(M_i=M/m_i\)。
\[x_0=\sum_{i=1}^n a_iM_i\times inv(m_i,M_i) \]通解为 \(\forall x=x_0+kM(k\in \mathbb{Z})\)
如果不保证所有 \(m_i\) 两两互质,同样有办法求解。
扩展中国剩余定理:模板
剖析原定理本质,其核心部分是:每次尝试构造一个数,使得这个数以某种方式融入答案后保证只对当前的 \((a_i,m_i)\) 有影响,同时保证 \(x\equiv a_i\pmod {m_i}\)。
利用这个思想,我们不难思考出以下算法:
假设已经求出了前 \(k-1\) 个方程的解为 \(x\),记 \(m={\rm lcm}(m_1,m_2,...,m_{k-1})\)。
显然 \(x+i\times m(i\in \mathbb{Z})\) 为前 \(k-1\) 个方程的通解。
那么目标变为求一个整数 \(t\),使得 \(x+t\times m\equiv a_k\pmod {m_k}\)。
利用扩展欧几里得算法不难计算出 \(t\),倘若无解则直接输出 \(-1\)。
求出之后令 \(x=x+t\times m\),继续求解即可。
所以最坏时间复杂度为 \(O(n\log m)\)其中 \(m={\rm lcm}(m_1,m_2,...,m_n)\)。
放一下代码吧:
LL exgcd(LL a,LL b,LL &x,LL &y){
if(!b){x=1,y=0;return a;}
LL d=exgcd(b,a % b,x,y);
LL z=x;x=y;y=z-y*(a/b);
return d;
}
LL exCRT(){
LL x,y;
LL ans=s[1],m=mod[1];
for(int i=2;i<=n;i++){
LL a=m,b=mod[i];
LL G=exgcd(a,b,x,y);
LL c=(s[i] - ans%b + b) % b;
if(c%G!=0) return -1;
LL P=b/G;
x=((x*c/G)%P+P)%P;
ans+=x*m;
m*=P;
ans=(ans%m+m)%m;
}
return (ans%m+m)%m;
}
int main(){
n=read();
for(int i=1;i<=n;i++) mod[i]=read(),s[i]=read();
printf("%lld\n",exCRT());
return 0;
}
运用:求解线性同余方程组。
【卢卡斯定理】
卢卡斯定理:
对于质数 \(p\):
\[C_n^m=C_{n/p}^{m/p}\times C_{n\ {\rm mod}\ p}^{m\ {\rm mod}\ p} \pmod p \]
代码:
LL Lucas(LL n, LL m, LL p){
if(n < m) return 0;
if(n == 0) return 1;
return Lucas(n/p, m/p, p) * C(n%p, m%p, p) % p;
}
应用:模数为质数且较小,但 \(n,m\) 较大时的组合数求解。
【正题】
终于到正题了
正题比前置芝士短系列
【例题一】
给出多重集 \(S=\{a_1·s_1,a_2·s_2,\cdots,a_k·s_k\}\)(由 \(a_1\) 个 \(s_1\),\(a_2\) 个 \(s_2\cdots\) 组成)
设 \(n=\sum_{i=1}^k a_i\),求从这个集合中选出 \(r\) 个的组合数。
满足 \(0\leq r\leq n\),但不保证 \(r\leq a_i(1\leq i\leq k)\)。
倘若题目保证 \(r\leq a_i(1\leq i\leq k)\)。
那么有:
证明方法较多,我的理解是将问题转化为集合 \(S=\{k-1·1,r·0\}\) 的排列数。
如果不保证,我们可以利用容斥原理减去不合理的情况。
有:
这是简单的容斥原理。
众所周知,容斥原理的时间复杂度为 \(O(2^k)\),代码实现时直接枚举二进制子集即可。
同时利用费马小定理预处理逆元,从而 \(O(m)\) 求解 \(C_{n}^m\)。
总时间复杂度:\(O(n2^n)\)。
typedef long long LL;
const LL MOD = 1e9 + 7;
LL n, m;
LL a[30], inv[30];
LL Pow(LL a, LL b){
LL sum = 1;
for(; b; b >>= 1){
if(b & 1) sum = sum * a % MOD;
a = a * a % MOD;
}
return sum;
}
LL C(LL a, LL b){
if(a<0 || b<0 || a<b) return 0;
a %= MOD;
if(a==0 || b==0) return 1;
LL sum = 1;
for(LL i=a; i>=a-b+1; i--) sum = sum * i % MOD;
for(int i=1; i<=b; i++) sum = sum * inv[i] % MOD;
return sum;
}
int main(){
for(int i=1; i<=20; i++)
inv[i] = Pow(i, MOD - 2);
scanf("%lld %lld", &n, &m);
for(int i=1; i<=n; i++) scanf("%lld", &a[i]);
LL ans = C(n+m-1, n-1);
for(int i=1; i<(1 << n); i++){
LL t = n+m-1;
int p = 0;
for(int j=0; j<n; j++)
if(i >> j & 1){
p++;
t -= (a[j+1] + 1);
}
if(p & 1) ans = (ans - C(t, n-1)) % MOD;
else ans = (ans + C(t, n-1)) % MOD;
}
printf("%lld\n", (ans + MOD) % MOD);
return 0;
}
【例题二】
求:
\[g^{\sum_{k|n} C_{n}^{k}}{\rm mod}\ 999911659 \]
其中模数是一个质数,那么根据扩展欧拉定理:
主要是求指数:\(\sum_{k|n} C_{n}^{k}{\rm mod\ 999911658}\)。
可惜卢卡斯定理只能运用于模数较小且为质数的情况下。
但值得一提的是:\(999911658=2\times 3\times 4679\times 35617\)。
对于这类质因子指数都为 \(1\) 的模数,求组合数时的一个常见的套路是:
若想要求 \(C_n^m\ {\rm mod}\ p\),\(a,p\) 又非常大,但 \(p\) 的每个质因子较小,且满足以上性质。
设 \(p=\Pi_{i=1}^n p_i\)。
先对于每个质因子 \(p_i\),利用卢卡斯定理求出 \(a_i=C_n^m\ {\rm mod}\ p_i\)。
最后解线性同余方程组:
\[\left\{\begin{aligned}x\equiv\ a_1\pmod {p_1} \quad\\ x\equiv\ a_2\pmod {p_2} \quad\\ x\equiv\ a_3\pmod {p_3} \quad\\ ...\quad\\x\equiv\ a_n\pmod {p_n} \quad\end{aligned}\right. \]那么 \(x\equiv C_n^m\pmod p\)。
至此问题完美解决。
本题十分综合,用到了:中国剩余定理、扩展欧几里得算法、乘法逆元、中国剩余定理和卢卡斯定理。
果然毒瘤
typedef long long LL;
LL n, g, MOD = 999911659 - 1;
LL a[4], fac[40010];
LL P[4] = {2, 3, 4679 ,35617};
void Init(LL p){
fac[0] = 1;
for(int i=1; i<=P[3]; i++) fac[i] = fac[i-1] * i % p;
}
LL Pow(LL a, LL b, LL p){
LL sum=1;
for(; b; b >>= 1){
if(b & 1) sum = sum * a % p;
a = a * a % p;
}
return sum;
}
LL Exgcd(LL a, LL b, LL &x, LL &y){
if(!b) {x=1, y=0; return a;}
LL d = Exgcd(b, a % b, x, y);
LL z = x; x = y; y = z - y * (a / b);
return d;
}
LL Inv(LL a, LL p){
LL x, y;
Exgcd(a, p, x, y);
return (x % p + p) % p;
}
LL C(LL n, LL m, LL p){
if(n < m) return 0;
return fac[n] * Inv(fac[m], p) % p * Inv(fac[n-m], p) % p;
}
LL Lucas(LL n, LL m, LL p){
if(n < m) return 0;
if(n == 0) return 1;
return Lucas(n/p, m/p, p) * C(n%p, m%p, p) % p;
}
int main(){
scanf("%lld %lld", &n, &g);
if(g % (MOD + 1) == 0){
puts("0");
return 0;
}
for(int j = 0; j<4; j++){
Init(P[j]);
for(LL i=1; i*i<=n; i++)
if(n % i == 0){
a[j] = (a[j] + Lucas(n, i, P[j])) % P[j];
if(i * i != n)
a[j] = (a[j] + Lucas(n, n/i, P[j])) % P[j];
}
}
LL ans=0;
for(int i=0; i<4; i++){
LL M = MOD / P[i];
LL inv = Inv(M, P[i]);
ans = (ans + inv * M % MOD * a[i] % MOD) % MOD;
}
MOD ++;
printf("%lld\n", Pow(g, ans, MOD));
return 0;
}
【总结】
简单数论小结。
引用资料&特别鸣谢:
- OI wiki数论部分。
- 《算法竞赛进阶指南》
完结撒花