数论学习笔记
TH911
·
·
算法·理论
大概率更好的阅读体验
本文可能会不定期更新。若存在内容正确性上的问题,请联系修复。
:::info[更新/修正记录]
本文中出现的代数式、数,如无特殊说明,均为整数。
对于 OI 中的数论题,答案不对请先尝试:
#define int __int128
typedef long long ll;
#define ll __int128
数论函数指的是定义域为正整数的函数,也可以视为一个序列。
基础部分
对于一个序列 $f_n=af_{n-1}+bf_{n-2}$,若 $\gcd(a,b)=1$,则有:
$$
\gcd(f_x,f_y)=f_{\gcd(x,y)}
$$
# 质数
## Fermat 素性测试
由费马小定理,对于质数 $p$,若 $\gcd(a,p)=1$,满足 $a^{p-1}\equiv 1\pmod p$,$a^{p-2}\equiv a^{-1}\pmod p$。
但是 $a^{p-1}\equiv1\pmod p$ 并不能推出 $p$ 为质数。
因此可以随机选择来测试。
## Miller–Rabin 素性测试
Miller-Rabin 素性测试,取质数集合 $A=\lbrace{2,3,5,7,11,13,17,19,23,29,31,37}\rbrace$,则可以通过随机化确定 `long long` 范围($[0,2^{64})$)内的任意整数 $n$ 是否为质数。
从 $A$ 中取一底数 $a$,若:
* $n=a$,$n$ 为质数。
* $n\bmod a=0\land n>a$,$n$ 不为质数。
在都不成立的情况下,进行 Miller-Rabin 素性测试。
将 $n-1$ 转化:
$$
n-1=d\times2^r
$$
其中,$d,r\in\N^*$,也就是说,$d$ 是 $n-1$ 在二进制位上的一个前缀,满足前缀的后缀不为 $0$。
### 二次探测定理
$$
x^2\equiv1\pmod p\\
\Downarrow\\
x\equiv\pm1\pmod p
$$
使用平方差公式可证。
### Miller-Rabin 素性测试的实现
基于 $a$ 执行 $r$ 轮测试,通过二次探测定理判断。
### 参考代码
```cpp
constexpr const ll A[12]={2,3,5,7,11,13,17,19,23,29,31,37};
bool MillerRabin(ll n){
if(n<=1){
return false;
}
for(ll a:A){
if(n==a){
return true;
}
if(n%a==0){
return false;
}
bool no=true;
ll d=n-1,r=0;
while(!(d&1)){
r++;
d>>=1;
}
ll pl=qpow(a,d,n);
if(pl==1){
no=false;
}
for(int i=0;no&&i<r;i++){
if(pl==n-1){
no=false;
}
pl=(__int128)pl*pl%n;
}
if(no){
return false;
}
}
return true;
}
```
# 最大公约数
## 裴蜀定理
对于 $\forall a,b\in \Z$:
* $\exist x,y\in \Z$,使 $ax+by=\gcd(a,b)$。
* $\forall x,y\in\Z$,$\gcd(a,b)\mid(ax+by)$。
### 逆定理
$\forall a,b\in \Z$,若 $d>0$ 且 $d$ 为 $a,b$ 的公因数,且存在 $x,y$,使得 $ax+by=d$,则:
$$
d=\gcd(a,b)
$$
# 乘法逆元
若存在 $ax\equiv 1\pmod p$,则称 $x$ 为 $a$ 关于 $p$ 的逆元,记 $x=a^{-1}$。
当 $\gcd(a,p)=1$ 的时候($p$ 为质数)逆元**一定存在**。
## 费马小定理
对于质数 $p$,若 $\gcd(a,p)=1$,满足 $a^{p-1}\equiv 1\pmod p$,$a^{p-2}\equiv a^{-1}\pmod p$。
常用于分数取模,即:
$$
\frac{a}{b}\equiv ab^{p-2}\pmod p
$$
详见[费马小定理](https://www.cnblogs.com/TH911/p/-/Fermat-Little-Therorem)。
## 欧拉定理
:::info[欧拉函数]
令 $n=p_1^{c_1}p_2^{c_2}p_3^{c_3}\cdots p_k^{c_k}$,满足 $p_i$ 为质数,$c_i>0$。
令 $n=p_1^{c_1}p_2^{c_2}p_3^{c_3}\cdots p_k^{c_k}$,满足 $p_i$ 为质数,$c_i>0$。
则:
$$
\varphi(n)=n\left(1-\frac 1{p_1}\right)\left(1-\frac 1{p_2}\right)\left(1-\frac 1{p_3}\right)\cdots\left(1-\frac 1{p_k}\right)
$$
推导见[容斥原理求解欧拉函数](https://www.cnblogs.com/TH911/p/-/Combinatorics#求解欧拉函数-varphin)。(组合数学没搬,放的原 Blog 链接)
:::
若 $\gcd(a,p)=1$,则 $a^{\varphi(p)}\equiv1\pmod p$。
显然,当 $p$ 为质数时,有 $\varphi(p)=p-1$,即费马小定理。因此,**费马小定理为欧拉定理的特殊形式**。
## 扩展欧拉定理
对于任意的 $a,p,b\in \N^*$,若满足 $\gcd(a,p)>1,b\geq \varphi(p)$,有:
$$
a^b\equiv a^{b\bmod \varphi(p)+\varphi(p)}\pmod p
$$
当 $\gcd(a,p)=1$ 时即欧拉定理。
可以使用初等证明或群论证明。
常用于降幂。
## 扩展欧几里得算法 exgcd
**注意不是“类欧几里得算法”。**
用于求关于 $x,y$ 的不定方程 $ax+by=\gcd(a,b)$ 的**一组整数特解**。
不妨令 $a>b$。
考虑到 $a=\left\lfloor\dfrac{a}{b}\right\rfloor b+a\bmod b$,代入可得:
$$
\left(\left\lfloor\frac{a}{b}\right\rfloor b+a\bmod b\right)x+by=\gcd(a,b)
$$
因此:
$$
b\left(\left\lfloor\frac ab\right\rfloor x+y\right)+(a\bmod b)x=\gcd(b,a\bmod b)
$$
令 $a'=b,x'=\left\lfloor\dfrac ab\right\rfloor x+y,b'=a\bmod b,y'=x$,有:
$$
a'x'+b'y'=\gcd(a',b')
$$
显然,可以**递归求解**。
那么直到 $b=0$ 时,可以直接解得一组整数特解 $\begin{cases}x=1\\y=0\end{cases}$。
设最终递归出来的解为 $\begin{cases}x=x_0\\y=y_0\end{cases}$。
令 $\Delta b=\left\vert\dfrac{b}{\gcd(a,b)}\right\vert,\Delta a=\left\vert\dfrac a{\gcd(a,b)}\right\vert$,则对于 $\forall k\in \N^*$,方程存在一组解为 $\begin{cases}x=x_0+k\Delta b\\y=y_0-k\Delta a\end{cases}$。
### 参考代码
```cpp
void exgcd(int a,int &x,int b,int &y){
if(!b){
x=1;
y=0;
return;
}
int tmp;
exgcd(b,tmp,a%b,x);
y=tmp-a/b*x;
}
```
### exgcd 求乘法逆元
如果需要讨论 $a$ 模 $p$ 意义下的乘法逆元 $a^{-1}$,显然有 $\gcd(a,p)=1$。
那么可列方程 $ax+py=1$,显然 $ax\equiv1\pmod p$。
则通过扩展欧几里得算法求出 $x$,即求出了 $a$ 的逆元。
> [例题链接:[NOIP 2012] 同余方程](https://www.luogu.com.cn/problem/P1082)
求出 $x$ 后,为了使 $x$ 为正整数,只需要让 $x\leftarrow(x\bmod p+p)\bmod p$ 即可。
:::success[参考代码]
```cpp
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
void exgcd(int a,int &x,int b,int &y){
if(!b){
x=1;
y=0;
return;
}
int tmp;
exgcd(b,tmp,a%b,x);
y=tmp-a/b*x;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int a,b;
cin>>a>>b;
int x0,y0;
exgcd(a,x0,b,y0);
x0%=b;
if(x0<0){
x0+=b;
}
cout<<x0<<'\n';
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
```
:::
### 例题:正整数最大/小解
> [例题链接](https://www.luogu.com.cn/problem/P5656)
>
> 给定不定方程 $ax+by=c$,$a,b,c\in \N^*$,判断其是否有解。
>
> 若有正整数解,分别求 $x,y$ 的最大/小值。
>
> 否则,求 $x,y$ 得最小正整数解。
对于无解,即 $\gcd(a,b)\nmid c$,很好判断。
先通过 exgcd 求出一组特解 $(x_0,y_0)$,则 $\begin{cases}x=x_0+k\Delta b\\y=y_0-k\Delta a\end{cases}$ 也是原方程的解。$\Delta b$ 为 $b$ 的单次变化量,最小为 $\Delta b=\left\vert\dfrac{b}{\gcd(a,b)}\right\vert$,$\Delta a=\left\vert\dfrac a{\gcd(a,b)}\right\vert$。
先求 $x$ 的最小正整数解 $x_{\min}$,即 $x_0+k\Delta b$ 最小,显然 $x_{\min}=x_0\bmod \Delta b$。
但是为了保证 $x_{\min}$ 为正整数,因此当 $x_0\bmod \Delta b\leq0$ 时,$x_{\min}=x_0\bmod \Delta b+\Delta b$。
$a,b>0$,因此 $x$ 最小时 $y$ 最大,因此可以确定 $y_{\max}=\dfrac{c-ax_{\min}}{b}$。
同理,可求出 $x_{\max},y_{\min}$。
当 $x_{\min}$ 为**正整数**,此时若 $y_{\max}>0$,则存在正整数解 $\begin{cases}x=x_{\min}\\y=y_{\max}\end{cases}$,因此可以判断正整数解(判断 $x_{\max}>0$ 也行)。
正整数解的个数即:
$$
\frac{x_{\max}-x_{\min}}{\Delta b}+1
$$
因为从 $x_{\min}$ 开始,$x_{\min}+k\Delta b$ 对应一组 $x,y$。
:::success[参考代码]
```cpp
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
#define int ll
typedef long long ll;
int gcd(int a,int b){
while(b){
int tmp=a;
a=b;
b=tmp%b;
}
return a;
}
void exgcd(int a,int &x,int b,int &y){
if(!b){
x=1;
y=0;
return;
}
int tmp;
exgcd(b,tmp,a%b,x);
y=tmp-a/b*x;
}
main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int T;
cin>>T;
while(T--){
int a,b,c;
cin>>a>>b>>c;
int gcdAB=gcd(a,b);
if(c%gcdAB){
cout<<"-1\n";
}else{
int w=c/gcdAB;
int x0,y0;
exgcd(a,x0,b,y0);
x0*=w,y0*=w;
// cerr<<"x0="<<x0<<" y0="<<y0<<endl;
int xMin,xMax,yMin,yMax;
int deltaA=a/gcdAB;
int deltaB=b/gcdAB;
xMin=x0%deltaB;
if(xMin<=0){
xMin+=deltaB;
}
yMax=(c-a*xMin)/b;
yMin=y0%deltaA;
if(yMin<=0){
yMin+=deltaA;
}
xMax=(c-b*yMin)/a;
// cerr<<"A="<<a<<" B="<<b<<endl;
// cerr<<"ΔA="<<deltaA<<" ΔB="<<deltaB<<endl;
// cerr<<"xMax="<<xMax<<" xMin="<<xMin<<" yMax="<<yMax<<" yMin="<<yMin<<endl;
if(yMax>0){
cout<<(xMax-xMin)/deltaB+1<<' '<<xMin<<' '<<yMin<<' '<<xMax<<' '<<yMax<<'\n';
}else{
cout<<xMin<<' '<<yMin<<'\n';
}
}
}
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
/*
1
3 18 6
2 1
*/
```
:::
### exgcd 与同余方程
exgcd 求解的是不定方程:
$$
ax+by=\gcd(a,b)
$$
而同余方程形如:
$$
ax\equiv c\pmod b
$$
可以将同余方程转化为:
$$
ax+by=c
$$
进而使用 exgcd 求解。
## 线性求逆元
有一种方法,可以在 $\mathcal O(\log V+n)$ 的时间内求出任意 $n$ 个数 $a_1,a_2,\cdots,a_n$ 关于 $p$ 的逆元。
记:
$$
\textit{pre}_i\equiv\prod_{j=1}^ia_j\pmod p
$$
那么就可以 $\mathcal O\left(\log V\right)$ 计算:
$$
\textit{preinv}_{n}\equiv\frac{1}{\textit{pre}_n}\pmod p
$$
$n-1\sim 1$ 递推,可以推出:
$$
\textit{preinv}_i\equiv\frac{1}{\textit{pre}_i}\equiv\frac{1}{\textit{pre}_{i+1} }\cdot a_{i+1}\pmod p
$$
随后再次递推,可以得到 $a_i$ 关于 $p$ 的逆元 $\textit{inv}_i$:
$$
\textit{inv}_i\equiv\textit{preinv}_i\cdot\textit{pre}_{i-1}\pmod p
$$
### 参考代码
```cpp
pre[0]=1;
for(int i=1;i<=n;i++){
pre[i]=1ll*pre[i-1]*a[i]%P;
}
inv[n]=qpow(pre[n],P-2);
for(int i=n-1;i>=1;i--){
inv[i]=1ll*inv[i+1]*(a[i+1])%P;
}
for(int i=1;i<=n;i++){
inv[i]=1ll*inv[i]*pre[i-1]%P;
}
```
# CRT 中国剩余定理
:::info[CRT 中的运算溢出]
**在使用 CRT 或 exCRT 时,一律建议使用 `__int128`,防止溢出**。
尽管题目大多会保证 $\operatorname{lcm}(p_1,p_2,\cdots,p_n)\leq10^{18}$,但是中间计算时会溢出。
:::
CRT(中国剩余定理)被用于求解线性同余方程组:
$$
\begin{cases}
x\equiv a_1\pmod{p_1}\\
x\equiv a_2\pmod{p_2}\\
\cdots\\
x\equiv a_n\pmod{p_n}
\end{cases}
$$
其中,$p_1,p_2,p_3,\cdots,p_n$ **两两互质**。
***
令 $L=\prod\limits_{i=1}^np_i$,则对于 $k\in\N$ 有 $k\cdot L\equiv0\pmod{p_i}$。
记 $q_i=\dfrac{L}{p_i}$,设 $c_i\equiv q_i^{-1}\pmod{p_i}$,即 $q_i$ 模 $p_i$ 意义下的逆元。
则,方程组的**最小**整数解为:
$$
x=\left(\sum_{i=1}^na_iq_ic_i\right)\bmod L
$$
同时,对于 $\forall k\in\N^*$,$x+kL$ 均为原方程组的解。
***
CRT 其实就是一个**构造式**的做法,易证 $\forall i\neq j,a_iq_ic_i\equiv0\pmod{p_j}$。
## 例题:[中国剩余定理(CRT)/ 曹冲养猪](https://www.luogu.com.cn/problem/P1495)
题目中的 $a_i,b_i$ 即分别为 $p_i,a_i$。
直接套 CRT 即可,但是需要注意的是,计算 $a_iq_ic_i$ 时需要 `__int128`,会爆 `long long`。
:::success[参考代码]
```cpp
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int N=10;
int n,a[N+1],p[N+1];
void exgcd(ll a,ll &x,ll b,ll &y){
if(!b){
x=1;
y=0;
return;
}
ll tmp;
exgcd(b,tmp,a%b,x);
y=tmp-a/b*x;
}
ll inverse(ll a,ll p){
ll x,y;
exgcd(a,x,p,y);
x%=p;
if(x<0){
x+=p;
}
return x;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
ll L=1;
for(int i=1;i<=n;i++){
cin>>p[i]>>a[i];
L*=p[i];
}
ll x=0;
for(int i=1;i<=n;i++){
ll q=L/p[i],c=inverse(q,p[i]);
x=(x+(__int128)1*a[i]%L*q%L*c)%L;
}
if(x<0){
x+=L;
}
cout<<x<<'\n';
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
```
:::
## 扩展 CRT/exCRT
> [例题链接](https://www.luogu.com.cn/problem/P4777)
exCRT 实际上与 CRT 关系不大,如同 exLucas 与 Lucas 一般。
exCRT 解决的是当模数 $p_1,p_2,p_3,\cdots,p_n$ **不一定两两互质**的时候(这时有可能无解)。
先考虑两个**同余方程**:
$$
\begin{cases}
x\equiv a_1\pmod{p_1}\\
x\equiv a_2\pmod{p_2}\\
\end{cases}
$$
设 $r,s$,将其转化为两个**不定方程**:
$$
\begin{cases}
x=a_1+r\cdot p_1\\
x=a_2+s\cdot p_2\\
\end{cases}
$$
因此可得 $r\cdot p_1-s\cdot p_2=a_2-a_1$。
由裴蜀定理,若 $(a_2-a_1)\nmid\gcd(p_1,-p_2)$,则该不定方程**无解**,则**原同余方程组无解**。
否则通过 exgcd 求出 $r,s$,则:
$$
x=a_1+r\cdot p_1=a_2+s\cdot p_2
$$
既然 $x=a_1+r\cdot p_1$,因此有:
$$
x\equiv a_1+r\cdot p_1\pmod{\operatorname{lcm}(p_1,p_2)}
$$
这样取 $\operatorname{lcm}(p_1,p_2)$ 是为了将两个方程合并。
这样,两两顺次合并,**最终合并为一个方程**即可解。
### 参考代码
```cpp
for(int i=2;i<=n;i++){
ll w=(a[i]-a[i-1])/gcd(p[i-1],-p[i]);
ll r,s;
exgcd(p[i-1],r,-p[i],s);
r*=w,s*=w;
a[i]=(p[i-1]*r+a[i-1])%lcm(p[i-1],p[i]);
p[i]=lcm(p[i-1],p[i]);
if(i==n){
if(a[n]<0){
a[n]+=p[n];
}
cout<<a[n]<<'\n';
}
}
```
:::success[参考代码]
并没有判断无解。
```cpp
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
namespace IO{
inline char getchar(){
static int p1,p2;
static char buf[1<<20];
if(p1==p2)p2=fread(buf,1,1<<20,stdin),p1=0;
return (p1==p2?EOF:buf[p1++]);
}
template<typename T>
inline void scanf(T &x){
x=0;
register int f=1;
register char ch=IO::getchar();
for(;ch<'0'||'9'<ch;ch=IO::getchar());
for(;'0'<=ch&&ch<='9';ch=IO::getchar())x=(x<<3)+(x<<1)+ch-'0';
x*=f;
}
static int p;
static char pbuf[1<<20];
inline void putchar(char ch){
pbuf[p++]=ch;
if(p==(1<<20)){
fwrite(pbuf,1,1<<20,stdout);
p=0;
}
}
template<typename T>
inline void printf(T x){
static char s[101];
int top=0;
do{
s[++top]=x%10+'0';
x/=10;
}while(x);
while(top)IO::putchar(s[top--]);
}
inline void end(){
fwrite(pbuf,1,p,stdout);
}
struct Tool{
~Tool(){
end();
}
}tool;
}
typedef __int128 lll;
#define int lll
#define ll lll
//typedef long long ll;
constexpr const int N=1e5;
int n;
ll a[N+1],p[N+1];
void exgcd(ll a,ll &x,ll b,ll &y){
if(!b){
x=1;
y=0;
return;
}
ll tmp;
exgcd(b,tmp,a%b,x);
y=tmp-a/b*x;
}
ll gcd(ll a,ll b){
while(b){
ll tmp=a;
a=b;
b=tmp%b;
}
return a;
}
ll lcm(ll a,ll b){
return a/gcd(a,b)*b;
}
main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
/*ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);*/
IO::scanf(n);
for(int i=1;i<=n;i++){
IO::scanf(p[i]);
IO::scanf(a[i]);
}
//合并(i-1,i)
for(int i=2;i<=n;i++){
ll w=(a[i]-a[i-1])/gcd(p[i-1],-p[i]);
ll r,s;
exgcd(p[i-1],r,-p[i],s);
r*=w,s*=w;
a[i]=(p[i-1]*r+a[i-1])%lcm(p[i-1],p[i]);
p[i]=lcm(p[i-1],p[i]);
if(i==n){
if(a[n]<0){
a[n]+=p[n];
}
IO::printf(a[n]);
}
}
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
```
:::
### exexCRT
即形如:
$$
\begin{cases}
f_1x&\equiv a_1\pmod{p_1}\\
f_2x&\equiv a_2\pmod{p_2}\\
f_3x&\equiv a_3\pmod{p_3}\\
&\cdots\\
f_nx&\equiv a_n\pmod{p_n}\\
\end{cases}
$$
多了一个**系数** $f_i$。
同样可以用类似 exCRT 的方法两两合并解决,[具体见此](https://www.cnblogs.com/TH911/p/-/P4774)。
# 离散对数
所谓离散对数,即模意义下取对数。
形如:给定模数 $p$,及**整数** $a,b$,求**整数** $x$ 使得 $a^x\equiv b\pmod p$。
**注意:离散对数可能不存在**。
## BSGS 算法
在 OI 中,BSGS 算法(Baby-Step Giant-Step,大步小步算法~~北上广深算法~~)常用来求解离散对数。
BSGS 算法要求 $a\perp p$,求 $x$ 使得 $a^x\equiv b\pmod p$。
若有解,则存在 $x\leq\varphi(p)$ 的解;因为欧拉定理说明,$a\perp p$ 时,$a^{\varphi(p)}\equiv1\pmod p$。
若枚举 $x$ 是 $\mathcal O(\varphi(p))$ 的,当 $p$ 为质数的时候**不能接受**,因此可以考虑分块。
令 $B=\left\lceil\sqrt{\varphi(p)}\right\rceil$,$x=qB+r$,且 $0\leq q,r\leq B$。
则有:
$$
a^{qB+r}\equiv b\pmod p
$$
$a\perp p$,则 $a$ 的乘法逆元 $a^{-1}$ 一定存在,有:
$$
a^{qB}\equiv b\left(a^{-1}\right)^r\pmod p
$$
枚举 $r$,将其与其对应的 $b\left(a^{-1}\right)^r$ 一同存储在数据结构(一般是哈希表)中,随后枚举 $q$,在数据结构中找 $a^{qB}$ 对应的 $r$,找到了 $r$ 便找到了一个解 $x=qB+r$。同时,因为 $a\perp p$,因此 $a^{-1},a^{-2},a^{-3},\cdots,a^{-B}$ 模 $p$ 意义下**互不相同**。
时间复杂度:$\mathcal O\left(\sqrt{\varphi(p)}\right)$,在 $p$ 为质数时最劣,$\mathcal O\left(\sqrt p\right)$。
### 参考代码
```cpp
B=ceil(sqrt(euler(P)));
c=qpow(a,B);
for(int r=0,powR=1,R=qpow(a,P-2);r<B;r++){
m[1ll*b*powR%P]=r;
powR=1ll*powR*R%P;
}
for(int q=0,powC=1;q<=B;q++){
if(m.count(powC)){
cout<<q*B+m[powC]<<endl;
return 0;
}
powC=1ll*powC*c%P;
}
cout<<"no solution\n";
```
> [例题链接](https://www.luogu.com.cn/problem/P3846)
:::success[参考代码]
```cpp
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
#include<unordered_map>
using namespace std;
int a,b,P,B,c;
unordered_map<int,int>m;
int euler(int n){
int ans=n;
for(int i=2;1ll*i*i<=n;i++){
if(n%i==0){
ans=ans/i*(i-1);
while(n%i==0){
n/=i;
}
}
}
if(n>1){
ans=ans/n*(n-1);
}
return ans;
}
int qpow(int base,int n){
int ans=1;
while(n){
if(n&1){
ans=1ll*ans*base%P;
}
base=1ll*base*base%P;
n>>=1;
}
return ans;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>P>>a>>b;
B=ceil(sqrt(euler(P)));
c=qpow(a,B);
for(int r=0,powR=1,R=qpow(a,P-2);r<B;r++){
m[1ll*b*powR%P]=r;
powR=1ll*powR*R%P;
}
for(int q=0,powC=1;q<=B;q++){
if(m.count(powC)){
cout<<q*B+m[powC]<<endl;
return 0;
}
powC=1ll*powC*c%P;
}
cout<<"no solution\n";
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
```
:::
## exBSGS 扩展 BSGS 算法
exBSGS 可解决 $a\perp p$ 不成立的情况。
由扩展欧拉定理可知,若 $x$ 有解,则 $x\leq2\varphi(p)$ 范围内也有解。
首先特判掉 $x=0$ 的情况,即 $b=1$ 或 $p=1$。
考虑分块,令 $B=\left\lceil\sqrt{2\varphi(p)}\right\rceil,x=qB-r,0\leq q,r\leq B$,则有:
$$
a^{qB-r}\equiv b\pmod p\\
\Downarrow\\
a^{qB}\equiv b\cdot a^r\pmod p
$$
因此可以预处理 $a^{qB}\bmod p$,随后枚举 $r$;但是注意到前者是后者的**充分条件而不是充要条件**,因此找出来的 $x=qB+r$ 只是“**可能的解**”,**需要检验**。
对于多个不同的 $q$ 产生的模 $p$ 意义下相同的 $a^{qB}$,可以只存储 **$q$ 最小的**两个。
:::info[证明]
由扩展欧拉定理,$a^x$ 是在 $k\leq\varphi(p)$ 值后以 $\varphi(p)$ 为周期循环的。
循环周期内的 $a^{qB}$ 显然至多存储 $1$ 个,而非循环部分 $a^{qB}$ 也至多存储 $1$ 个。
即:保留最小的 $2$ 个。
:::
时间复杂度:仍然为 $\mathcal O\left(\sqrt{\varphi(p)}\right)$。
### 参考代码
```cpp
a%=P;b%=P;
if(b==1||P==1){
cout<<"0\n";
continue;
}
B=ceil(2*sqrt(euler(P)));
c=qpow(a,B);
for(int q=0,powC=1;q<B;q++){
auto &pl=m[powC];
if(pl.size()<2){
pl.push_back(q);
}
powC=1ll*c*powC%P;
}
int x=2147483647;
for(int r=0,powA=1;r<B;r++){
auto &pl=m[1ll*b*powA%P];
for(int q:pl){
int x0=(1ll*q*B-r)%P;
if(x0<0){
continue;
}
if(qpow(a,x0)==b){
x=min(x,x0);
}
}
powA=1ll*powA*a%P;
}
if(x<2147483647){
cout<<x<<'\n';
}else{
cout<<"No Solution\n";
}
```
> [例题链接](https://www.luogu.com.cn/problem/P4195)
:::success[参考代码]
```cpp
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
#include<unordered_map>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/tree_policy.hpp>
using namespace std;
int a,b,P,B,c;
//pb_ds 哈希表常数较 unordered_map 更小
__gnu_pbds::gp_hash_table<int,vector<int> >m;
//unordered_map<int,vector<int> >m;
//map<int,vector<int> >m;
int euler(int n){
int ans=n;
for(int i=2;1ll*i*i<=n;i++){
if(n%i==0){
ans=ans/i*(i-1);
while(n%i==0){
n/=i;
}
}
}
if(n>1){
ans=ans/n*(n-1);
}
return ans;
}
int qpow(int base,int n){
int ans=1;
while(n){
if(n&1){
ans=1ll*ans*base%P;
}
base=1ll*base*base%P;
n>>=1;
}
return ans;
}
main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
while(true){
m.clear();
cin>>a>>P>>b;
if(a==P&&P==b&&b==0){
break;
}
a%=P;b%=P;
if(b==1||P==1){
cout<<"0\n";
continue;
}
B=ceil(2*sqrt(euler(P)));
c=qpow(a,B);
for(int q=0,powC=1;q<B;q++){
auto &pl=m[powC];
if(pl.size()<2){
pl.push_back(q);
}
powC=1ll*c*powC%P;
}
int x=2147483647;
for(int r=0,powA=1;r<B;r++){
auto &pl=m[1ll*b*powA%P];
for(int q:pl){
int x0=(1ll*q*B-r)%P;
if(x0<0){
continue;
}
if(qpow(a,x0)==b){
x=min(x,x0);
}
}
powA=1ll*powA*a%P;
}
if(x<2147483647){
cout<<x<<'\n';
}else{
cout<<"No Solution\n";
}
}
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
```
:::
# Lucas 定理
## Lucas 定理
Lucas 定理用于求解大组合数取**质数模**。(对于模数不为质数的情况,请参考 exLucas。
Lucas 定理的内容很简单:
$$
\binom{n}{m}\equiv\binom{\lfloor\frac np\rfloor}{\lfloor\frac mp\rfloor}\binom{n\bmod p}{m\bmod p}\pmod p
$$
考虑如何证明。
### 证明
由二项式定理:
$$
(a+b)^p=\sum_{i=0}^p\binom pia^ib^{p-i}
$$
考虑 $\dbinom pi$ 在模 $p$ 意义下的取值。
因为:
$$
\dbinom pi=\dfrac{p!}{i!(p-i)!}
$$
那么如果化简之后,分子 $p!$ 中的 $p$ 项**没有**被约分掉,则有 $\dbinom pi\equiv p\cdot\dfrac{(p-1)!}{i!(p-i)!}\equiv0\pmod p$。
因为 $p$ 为质数,所以 $p$ 项能被约分掉当且仅当 $i!$ 中含有 $p$ 项或 $(p-i)!$ 中含有 $p$ 项。
即:$\dbinom pi\not\equiv0\pmod p$ 当且仅当 $i\equiv0\pmod p$ 或 $p-i\equiv 0\pmod p$。
在二项式定理中,$i$ 满足 $0\leq i\leq p$,所以 $\dbinom pi\not\equiv0$ 当且仅当 $i=0$ 或 $i=p$。
在这两种情况中,都可以计算得到 $\dbinom pi=1$,即 $\dbinom pi\equiv[i=0\lor i=p]$。
重新带回二项式定理,可得:
$$
\begin{aligned}
(a+b)^p&\equiv \dbinom{p}{0}a^0b^p+\dbinom ppa^pb^0\\
&\equiv a^p+b^p
\end{aligned}
\pmod p
$$
***
考虑一个二项式 $(1+x)^n\bmod p$。
$$
\begin{aligned}
(1+x)^n&\equiv (1+x)^{p\lfloor\frac np\rfloor+n\bmod p}\\
&\equiv(1+x)^{p\lfloor\frac np\rfloor}(1+x)^{n\bmod p}\\
&\equiv(1+x^p)^{\lfloor\frac np\rfloor}(1+x)^{n\bmod p}\\
\end{aligned}
\pmod p
$$
由二项式定理,$(1+x)^n$ 的 $x^m$ 项系数为 $\dbinom nm$。
而想要从 $(1+x^p)^{\lfloor\frac np\rfloor}(1+x)^{n\bmod p}$ 中得到 $x^m$,即从 $(1+x^p)^{\lfloor\frac np\rfloor}$ 中选取 $\lfloor\frac mp\rfloor$ 个 $x^p$,再从 $(1+x)^{n\bmod p}$ 中选取 $m\bmod p$ 个 $x$。
即:
$$
\dbinom nm\equiv\dbinom{\lfloor\frac np\rfloor}{\lfloor\frac mp\rfloor}\dbinom{n\bmod p}{m\bmod p}
\pmod p
$$
***
你可能有一个问题:这看起来明明应当是一个**等式**,但为什么是**同余**呢?
即,Lucas 定理应当表述为:
$$
\dbinom nm=\dbinom{\lfloor\frac np\rfloor}{\lfloor\frac mp\rfloor}\dbinom{n\bmod p}{m\bmod p}
$$
但是,你要知道,只有在模 $p$ 意义下才有 $(1+x)^{p\lfloor\frac np\rfloor}=(1+x^p)^{\lfloor\frac np\rfloor}$。
因此,只有在模 $p$ 意义下才有:
$$
\dbinom nm=\dbinom{\lfloor\frac np\rfloor}{\lfloor\frac mp\rfloor}\dbinom{n\bmod p}{m\bmod p}
$$
即:
$$
\dbinom nm\equiv\dbinom{\lfloor\frac np\rfloor}{\lfloor\frac mp\rfloor}\dbinom{n\bmod p}{m\bmod p}\pmod p
$$
### 应用
当 $n$ 比较大而无法使用其他方法(例如预处理 $1\sim n$ 的阶乘再利用乘法逆元)直接求解组合数时,可以使用 Lucas 定理。
Lucas 定理只需要递归使用即可,即递归计算 $\dbinom{\lfloor\frac np\rfloor}{\lfloor\frac mp\rfloor}$,递归终点即 $\dbinom{0}{0}$。
其时间复杂度为:$\mathcal O\left(f(p)\log m\right)$。
其中,$f(p)$ 表示单次计算 $\dbinom{n\bmod p}{m\bmod p}$ 的复杂度,因写法而异。
可以使用乘法逆元,则时间复杂度为 $\mathcal O(\log p\log m)$。
也可以 $\mathcal O(p\log p)$ 递推,时间复杂度为 $\mathcal O(p\log p\log m)$。
推荐使用乘法逆元。
> [例题链接](https://www.luogu.com.cn/problem/P3807)
很简单,注意是 $\dbinom{n+m}{n}$ 即可。
:::success[参考代码]
```cpp
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
const int N=1e5;
int ksm(int a,int n,int p){
if(n==0)return 1;
int t=ksm(a,n>>1,p);
t=1ll*t*t%p;
if(n&1)t=1ll*t*a%p;
return t;
}
int C(int n,int m,int p){
static int ans[N+1]={1};
for(int i=1;i<=m;i++){
ans[i]=1ll*ans[i-1]*(n-i+1)%p*ksm(i,p-2,p)%p;
}return ans[m];
}
int Lucas(int n,int m,int p){
if(m==0)return 1;
return (1ll*Lucas(n/p,m/p,p)*C(n%p,m%p,p))%p;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
int T,n,m,p;
scanf("%d",&T);
while(T--){
scanf("%d %d %d",&n,&m,&p);
printf("%d\n",Lucas(n+m,n,p));
}
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
```
:::
## exLucas 算法(exLucas 定理)
exLucas 算法可以求 $\dbinom{n}{m}\bmod P$,$P$ **不一定为质数**。(但是,exLucas 实际上和 Lucas 没有多大关系……)
由唯一分解定理,可以对 $P$ 进行质因数分解:
$$
P=p_1^{c_1}p_2^{c_2}p_3^{c_3}\cdots p_n^{c^n}
$$
### CRT 求解
我们如果能够求出 $a_1,a_2,\cdots,a_n$ 使得:
$$
\begin{cases}
\dbinom nm&\equiv a_1\pmod{p_1^{c_1}}\\
\dbinom nm&\equiv a_2\pmod{p_1^{c_2}}\\
&\cdots\\
\dbinom nm&\equiv a_n\pmod{p_n^{c_n}}\\
\end{cases}
$$
那么我们就可以通过 **CRT** 求解出 $\dbinom nm\bmod P$。因为在 CRT 中,恰好也有 $P=p_1^{c_1}p_2^{c_2}\cdots p_n^{c_n}$。
### 求解模质数幂下余数
展开定义式:
$$
\dbinom nm=\frac{n!}{m!(n-m)!}
$$
因此:
$$
\dbinom nm\equiv\frac{n!}{m!(n-m)!}\pmod{p_i^{c_i}}
$$
因为 $p_i^{c_i},m!,(n-m)!$ 不一定互质,因此**乘法逆元不一定存在**。
考虑先**提出**分子和分母中所有的 $p_i$ 次幂,随后便可以使用逆元求解。
记 $x$ 的质因数分解中**质数** $p$ 的幂次为 $\nu_p(x)$,剩余积为 $(x)_p$,即:
$$
x=p^{\nu_p(x)}\cdot(x)_p
$$
则有:
$$
\frac{n!}{m!(n-m)!}\equiv\frac{(n!)_{p_i}}{(m!)_{p_i}((n-m)!)_{p_i}}\cdot p_i^{\nu_{p_i}(n!)-\nu_{p_i}(m!)-\nu_{p_i}((n-m)!)}\pmod{p_i^{c_i}}
$$
那么,现在只需要考虑对于 $x,p$,如何**高效地**求出 $\nu_{p}(x!)\bmod p_i^{c_i},(x!)_p\bmod p_i^{c_i}$。
***
考虑:
$$
x!=1\times2\times\cdots\times p\times\cdots\times2p\times\cdots\times\left\lfloor\dfrac{x}{p}\right\rfloor p\times\cdots\times x
$$
容易发现,在 $p,2p,3p,\cdots,\left\lfloor\dfrac xp \right\rfloor p$ 中 $p$ 的个数为 $\left\lfloor\dfrac xp\right\rfloor+\nu_p\left(\left\lfloor\dfrac xp \right\rfloor!\right)$。其中 $\nu_p\left(\left\lfloor\dfrac xp \right\rfloor!\right)$ 是 $1,2,3,\cdots,\left\lfloor\dfrac xp\right\rfloor$ 中的 $p$ 的个数。
$p$ 为**质数**,所以在 $1,2,3,\cdots,p-1,p+1,\cdots$ 中不会含有 $p$。
将递推式展开:
$$
\begin{aligned}
\nu_p\left(x!\right)&=\left\lfloor\dfrac xp\right\rfloor+\nu_p\left(\left\lfloor\dfrac xp \right\rfloor!\right)\\
&=\left\lfloor\dfrac xp\right\rfloor+\left\lfloor\dfrac x{p^2}\right\rfloor+\nu_p\left(\left\lfloor\dfrac xp^2 \right\rfloor!\right)\\
&=\left\lfloor\dfrac xp\right\rfloor+\left\lfloor\dfrac x{p^2}\right\rfloor+\left\lfloor\dfrac x{p^3}\right\rfloor+\cdots
\end{aligned}
$$
因此可以 $\mathcal O(\log_p x)$ 计算 $\nu_p(x!)$:
```cpp
int v(ll n,ll p){
int ans=0;
do{
n/=p;
ans+=n;
}while(n);
return ans;//这里取不取模其实都无所谓,因为幂次不会太多,想取也可以.
}
```
***
现在考虑如何计算 $(x!)_p\bmod p^c$;显然不能利用定义式 $(x!)_p=\dfrac{x!}{\nu_p(x!)}$,而需要其他方法(因为无法得知 $x!$)。
不难进行递推:
$$
\begin{aligned}
(x!)_p&\equiv\prod_{i=1}^n(i)_p\\
&\equiv\left(\prod_{1\leq i\leq x,i\perp p}(i)_p\right)\left(\prod_{i=1}^{\left\lfloor\frac xp\right\rfloor}(p\cdot i)_p\right)\\
&\equiv\left(\prod_{1\leq i\leq x,i\perp p}(i)_p\right)\left(\left\lfloor\frac xp\right\rfloor!\right)_p\\
&\equiv\left(\prod_{1\leq i\leq p^c,i\perp p}i\right)^{\left\lfloor\frac{x}{p^c}\right\rfloor}\left(\prod_{1\leq i\leq x\bmod p^c,i\perp p}i\right)\left(\left\lfloor\frac xp\right\rfloor!\right)_p\\
\end{aligned}
$$
由 Wilson 定理的推论($m=2,4,p^c,2p^c$ 时 $k\equiv-1$,否则为 $1$):
$$
\prod_{1\leq k\leq m,k\perp m}k\equiv\pm1\pmod m
$$
因此:
$$
\begin{aligned}
(x!)_p&\equiv(\pm1)^{\left\lfloor\frac{x}{p^c}\right\rfloor}\left(\prod_{1\leq i\leq x\bmod p^c,i\perp p}i\right)\left(\left\lfloor\frac xp\right\rfloor!\right)_p
\end{aligned}
$$
可以发现,每次计算 $(x!)_p$ 时,$p^c$ 是固定的,因此可以预处理:
$$
f_j\equiv\prod_{1\leq i\leq j,i\perp p}i\pmod p^c
$$
因此,得到最终推导式:
$$
\begin{aligned}
(x!)_p&\equiv(\pm1)^{\left\lfloor\frac{x}{p^c}\right\rfloor}f_{x\bmod p^c}\left(\left\lfloor\frac xp\right\rfloor!\right)_p
\end{aligned}
$$
可以递归或迭代处理,时间复杂度 $\mathcal O(p^c+\log n x)$。
```cpp
int Wilson(int n,int p,int pc){
int ans=1;
vector<int>f(pc)
f[0]=1;
for(int i=1;i<pc;i++){
f[i]=i%p?f[i-1]*i%pc:f[i-1];
}
bool flag=p!=2||pc<=4;
while(n>1){
if((n/pc)&flag){
ans=pc-ans;
}
ans=ans*f[n%pc]%pc;
n/=p;
}
return ans;
}
```
> [例题链接](https://www.luogu.com.cn/problem/P4720)
:::success[参考代码]
```cpp
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
#define ll __int128
#define int __int128
constexpr const int PMAX=1e6;
void exgcd(ll a,ll &x,ll b,ll &y){
if(!b){
x=1;
y=0;
return;
}
ll tmp;
exgcd(b,tmp,a%b,x);
y=tmp-a/b*x;
}
ll inverse(ll a,ll p){
ll x,y;
exgcd(a,x,p,y);
x%=p;
if(x<0){
x+=p;
}
return x;
}
int qpow(int base,int n){
int ans=1;
while(n){
if(n&1){
ans=1ll*ans*base;
}
base=1ll*base*base;
n>>=1;
}
return ans;
}
int CRT(vector<int>a,vector<int>p){
ll L=1;
for(int &i:p){
L*=i;
}
ll x=0;
for(int i=0;i<a.size();i++){
ll q=L/p[i];
x=(x+1ll*a[i]*q%L*inverse(q,p[i])%L)%L;
}
if(x<0){
x+=L;
}
return x;
}
int v(ll n,ll p){
int ans=0;
do{
n/=p;
ans+=n;
}while(n);
return ans;
}
int Wilson(int n,int p,int pc){
int ans=1;
vector<int>f(pc);
f[0]=1;
for(int i=1;i<pc;i++){
f[i]=i%p?f[i-1]*i%pc:f[i-1];
}
bool flag=p!=2||pc<=4;
while(n>1){
if((n/pc)&flag){
ans=pc-ans;
}
ans=ans*f[n%pc]%pc;
n/=p;
}
return ans;
}
void breakDown(int P,vector<int>&p,vector<int>&pc){
int pp=P;
for(int i=2;i*i<=pp;i++){
if(pp%i==0){
p.push_back(i);
pc.push_back(1);
while(pp%i==0){
pc.back()*=i;
pp/=i;
}
}
}
if(pp>1){
p.push_back(pp);
pc.push_back(pp);
}
}
long long exLucas(ll n,ll m,ll P){
if(n<m){
return 0;
}
vector<int>p,pc;
breakDown(P,p,pc);
vector<int>a(pc.size());
for(int i=0;i<p.size();i++){
int nWilson=Wilson(n,p[i],pc[i]);
int mWilson=Wilson(m,p[i],pc[i]);
int nmWilson=Wilson(n-m,p[i],pc[i]);
a[i]=nWilson*inverse(mWilson*nmWilson%pc[i],pc[i])*qpow(p[i],v(n,p[i])-v(m,p[i])-v(n-m,p[i]));
}
return CRT(a,pc);
}
main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
long long n,m,P;
cin>>n>>m>>P;
cout<<exLucas(n,m,P)<<'\n';
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
/*
1000000000000000000 500000000000000000 998243
0
*/
```
:::
> [[国家集训队] 礼物](https://www.luogu.com.cn/problem/P2183)
:::success[参考代码]
```cpp
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
#define ll __int128
#define int __int128
constexpr const int M=5;
long long w[M+1];
constexpr const int PMAX=1e5;
void exgcd(ll a,ll &x,ll b,ll &y){
if(!b){
x=1;
y=0;
return;
}
ll tmp;
exgcd(b,tmp,a%b,x);
y=tmp-a/b*x;
}
ll inverse(ll a,ll p){
ll x,y;
exgcd(a,x,p,y);
x%=p;
if(x<0){
x+=p;
}
return x;
}
int qpow(int base,int n){
int ans=1;
while(n){
if(n&1){
ans=1ll*ans*base;
}
base=1ll*base*base;
n>>=1;
}
return ans;
}
int CRT(vector<int>a,vector<int>p){
ll L=1;
for(int &i:p){
L*=i;
}
ll x=0;
for(int i=0;i<a.size();i++){
ll q=L/p[i];
x=(x+1ll*a[i]*q%L*inverse(q,p[i])%L)%L;
}
if(x<0){
x+=L;
}
return x;
}
int v(ll n,ll p){
int ans=0;
do{
n/=p;
ans+=n;
}while(n);
return ans;
}
int Wilson(int n,int p,int pc){
int ans=1;
vector<int>f(pc);
f[0]=1;
for(int i=1;i<pc;i++){
f[i]=i%p?f[i-1]*i%pc:f[i-1];
}
bool flag=p!=2||pc<=4;
while(n>1){
if((n/pc)&flag){
ans=pc-ans;
}
ans=ans*f[n%pc]%pc;
n/=p;
}
return ans;
}
void breakDown(int P,vector<int>&p,vector<int>&pc){
int pp=P;
for(int i=2;i*i<=pp;i++){
if(pp%i==0){
p.push_back(i);
pc.push_back(1);
while(pp%i==0){
pc.back()*=i;
pp/=i;
}
}
}
if(pp>1){
p.push_back(pp);
pc.push_back(pp);
}
}
long long exLucas(ll n,ll m,ll P){
if(n<m){
return 0;
}
vector<int>p,pc;
breakDown(P,p,pc);
vector<int>a(pc.size());
for(int i=0;i<p.size();i++){
int nWilson=Wilson(n,p[i],pc[i]);
int mWilson=Wilson(m,p[i],pc[i]);
int nmWilson=Wilson(n-m,p[i],pc[i]);
a[i]=nWilson*inverse(mWilson*nmWilson%pc[i],pc[i])*qpow(p[i],v(n,p[i])-v(m,p[i])-v(n-m,p[i]));
}
return CRT(a,pc);
}
main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
long long n,m,P;
cin>>P>>n>>m;
long long ans=1;
for(int i=1;i<=m;i++){
cin>>w[i];
ans=ans*exLucas(n,w[i],P)%P;
n-=w[i];
if(n<0){
cout<<"Impossible"<<endl;
return 0;
}
}
cout<<ans<<'\n';
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
```
:::
# 数论分块
数论分块(整除分块)用于快速计算:
$$
\sum_{i=1}^nf(i)g\left(\left\lfloor\dfrac ni\right\rfloor\right)
$$
数论分块的原理即本质不同的 $\left\lfloor\dfrac ni\right\rfloor$ 只有至多 $\mathcal O\left(\sqrt n\right)$ 种。
:::info[证明]
* 当 $i\leq\sqrt n$ 时,$i$ 只有 $\mathcal O\left(\sqrt n\right)$ 种取值。
* 当 $i>\sqrt n$ 时,$\dfrac ni\leq\sqrt n$,$\left\lfloor\dfrac ni\right\rfloor$ 只有 $\mathcal O\left(\sqrt n\right)$ 种取值。
故,本质不同的 $\left\lfloor\dfrac ni\right\rfloor$ 只有至多 $\mathcal O\left(\sqrt n\right)$ 种取值。
:::
因此 $\left\lfloor\dfrac ni\right\rfloor$ 相同值肯定是连续的,那么就可以找出这 $\mathcal O\left(\sqrt n\right)$ 区间 $[l_1,r_1],[l_2,r_2],\cdots,[l_k,r_k]$($r_i+1=l_{i+1}$),求出:
$$
g\left(\left\lfloor\dfrac nl\right\rfloor\right)\sum_{j=l_i}^{r_i}f(j)
$$
将 $i$ 个答案相加即可。
若分块后可以 $\mathcal O(1)$ 求出,则数论分块是 $\mathcal O\left(\sqrt n\right)$ 的。
## 寻找区间
对于一个 $l$ 所对应的 $\left\lfloor\dfrac nl\right\rfloor$,要寻找一个最大的 $r$ 使得 $\left\lfloor\dfrac nl\right\rfloor=\left\lfloor\dfrac nr\right\rfloor$,从而找到区间 $[l,r]$。
则有:
$$
r=\left\lfloor\dfrac{n}{\left\lfloor\dfrac nl\right\rfloor}\right\rfloor
$$
而对于向上取整的情形,即对于 $l$ 对应的 $\left\lceil\dfrac nl\right\rceil$,要寻找一个最大的 $r$ 使得 $\left\lceil\dfrac nr\right\rceil$,从而找到区间 $[l,r]$。
则有:
$$
r=\left\lfloor\dfrac{n-1}{\left\lfloor\dfrac {n-1}l\right\rfloor}\right\rfloor
$$
## 单一参数
> [[CQOI2007] 余数求和](https://www.luogu.com.cn/problem/P2261)
>
> 给定 $n,k\in\N^*$,满足 $1\leq n,k\leq10^9$,求:
>
> $$
> G(n,k)=\sum_{i=1}^nk\bmod i
> $$
显然,$k\bmod i=k-i\left\lfloor\dfrac ki\right\rfloor$。
因此有:
$$
\begin{aligned}
G(n,k)&=\sum_{i=1}^n\left(k-i\left\lfloor\dfrac ki\right\rfloor\right)\\
&=nk-\sum_{i=1}^ni\left\lfloor\dfrac ki\right\rfloor
\end{aligned}
$$
考虑快速求出 $\sum\limits_{i=1}^ni\left\lfloor\dfrac ki\right\rfloor$,可以进行**数论分块**。
对于数论分块中一组确定的 $l,r$,有:
$$
\sum_{i=l}^ri\left\lfloor\dfrac ki\right\rfloor=\left\lfloor\dfrac kl\right\rfloor\sum_{i=l}^ri=\left\lfloor\dfrac ki\right\rfloor\dfrac{(l+r)(r-l+1)}{2}
$$
这可以 $\mathcal O(1)$ 计算,因此总计算时间复杂度为 $\mathcal O\left(\sqrt n\right)$。
:::success[参考代码]
```cpp
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
#define int ll
typedef long long ll;
ll G(int n,int k){
ll ans=1ll*n*k;
for(int l=1,r;l<=n;l=r+1){
int t=k/l;
if(!t){
r=n;
}else{
r=min(k/t,n);
}
ans-=1ll*(r-l+1)*t*(l+r)>>1;
}
return ans;
}
main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int n,k;
cin>>n>>k;
cout<<G(n,k)<<'\n';
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
```
## 多参数
此时寻找 $r$,会取 $\min$,即:
$$
r=\min\left(\left\lfloor\dfrac{n}{\left\lfloor\dfrac nl\right\rfloor}\right\rfloor,\left\lfloor\dfrac{m}{\left\lfloor\dfrac ml\right\rfloor}\right\rfloor,\cdots\right)
$$
> [[清华集训 2012] 模积和](https://www.luogu.com.cn/problem/P2260)
>
> 给定 $1\leq n,m\leq10^9$,求:
>
> $$
> \left(\sum_{i=1}^n\sum_{j=1}^m[i\neq j](n\bmod i)(m\bmod j)\right)\bmod 19940417
> $$
不妨令 $n\leq m$,否则交换 $n,m$。
模运算不好处理,转换一下:
$$
\sum_{i=1}^n\sum_{j=1}^m[i\neq j]\left(n-i\left\lfloor\dfrac ni\right\rfloor\right)\left(m-j\left\lfloor\dfrac mj\right\rfloor\right)
$$
$[i\neq j]$ 不好处理,因此可以先全部求出来,再减去相等的部分:
$$
\sum_{i=1}^n\sum_{j=1}^m\left(n-i\left\lfloor\dfrac ni\right\rfloor\right)\left(m-j\left\lfloor\dfrac mj\right\rfloor\right)-\sum_{i=1}^n\left(n-i\left\lfloor\dfrac ni\right\rfloor\right)\left(m-i\left\lfloor\dfrac mi\right\rfloor\right)
$$
不难发现 $j$ 与 $i$ 的取值**无关**,因此转换为:
$$
\sum_{i=1}^n\left(n-i\left\lfloor\dfrac ni\right\rfloor\right)\sum_{j=1}^m\left(m-j\left\lfloor\dfrac mj\right\rfloor\right)-\sum_{i=1}^n\left(n-i\left\lfloor\dfrac ni\right\rfloor\right)\left(m-i\left\lfloor\dfrac mi\right\rfloor\right)
$$
于是可以 $\mathcal O\left(\max(n,m)\right)$ 计算,但是考虑到 $1\leq n,m\leq10^9$,只需要使用数论分块优化即可。
:::success[参考代码]
```cpp
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
constexpr const int P=19940417,inv2=9970209,inv6=3323403;
#define ll __int128
//typedef long long ll;
ll g(ll n,ll m){
ll ans=0;
for(ll l=1,r=n;l<=n;l=r+1,r=n){
ll t=m/l;
if(t){
r=min(n,m/t);
}
ans=(ans+t*(l+r)%P*(r-l+1)%P*inv2%P)%P;
}
return ans;
}
ll g2(ll n,ll m){
ll ans=0;
for(ll l=1,r=n;l<=n;l=r+1,r=n){
ll tn=n/l,tm=m/l;
r=min(n/tn,m/tm);
ans+=m*(l+r)%P*(r-l+1)*inv2*tn%P;
ans+=n*(l+r)%P*(r-l+1)*inv2*tm%P;
ans-=(r*(r+1)*(2*r+1)%P-(l-1)*l*(2*l-1)%P)*inv6*tn*tm%P;
ans%=P;
}
return ans;
}
int f(ll n,ll m){
ll ans=(n*n%P-g(n,n))*(m*m%P-g(m,m))%P;
ans=(ans-n*n%P*m%P)%P;
ans=(ans+g2(n,m))%P;
if(ans<0){
ans+=P;
}
return ans;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int n,m;
cin>>n>>m;
if(n>m){
swap(n,m);
}
cout<<f(n,m)<<'\n';
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
```
:::
# 积性函数
## 定义
若数论函数 $f(n)$ 满足 $f(1)=1$,且对于 $\forall x,y\in\N^*\land x\perp y$,有 $f(xy)=f(x)\cdot f(y)$,称 $f(n)$ 为**积性函数**。
若数论函数 $f(n)$ 满足 $f(1)=1$,且对于 $\forall x,y\in\N^*$,有 $f(xy)=f(x)\cdot f(y)$,称 $f(n)$ 为**完全积性函数**。
## 基础部分
若 $f(n),g(n)$ 均为积性函数,则 $f(x^p),f^p(x),f(x)g(x),(f*g)(x)$ 均为积性函数。其中 $f*g$ 为迪利克雷卷积:
$$
(f*g)(x)=\sum_{d\mid x}f(d)g\left(\dfrac xd\right)
$$
### 常见积性函数
* **单位函数**:$\varepsilon(n)=[n=1]$,完全积性函数。(`\varepsilon`)
* **恒等函数**:$\mathrm{id}_{k}(n)=n^k$,$\mathrm{id}_1(n)$ 通常记为 $\mathrm{id}(n)$,完全积性函数。
* 常数函数:$1(n)=1$,完全积性函数。
* 除数函数:$\sigma_k(n)=\sum\limits_{d\mid n}d^k$,$\sigma_0(n)$ 通常记作 $d(n)$ 或 $\tau(n)$,$\sigma_1(n)$ 通常记作 $\sigma(n)$。(`\sigma`)
:::info[$d(n)$ 的求法]
令 $n=p_1^{c_1}p_2^{c_2}\cdots p_k^{c_k}$,其中 $p_1,p_2,\cdots,p_k$ 均为质数。
对于每一个指数 $c_i$ 的更改,都会产生新的因数。每一个 $i$,都有 $c_i+1$ 种选择。显然有:
$$
d(n)=\prod_{i=1}^k(c_i+1)
$$
时间复杂度 $\mathcal O\left(\sqrt n\right)$。
:::
* 欧拉函数:$\varphi(n)$。(`\varphi`)
* 莫比乌斯函数:
$$
\mu(n)=
\begin{cases}
1&n=1\\
0&\exist d>1,d^2\mid n\\
(-1)^k&k\text{ 为 }n\text{ 的本质不同质因子个数}
\end{cases}
$$
$\mu(n)=0$ 即 $n$ 含有平方因子。(`\mu`)
同时,除数函数 $d(n)$ 即 $n$ 的因数个数,且 $d(ij)$ 满足:
$$
\begin{aligned}
d(ij)&=\sum_{x\mid i}\sum_{y\mid j}[x\perp y]
\end{aligned}
$$
:::info[证明]
令 $i=p_1^{c_1}p_2^{c_2}\cdots p_k^{c_k},j={p'_1}^{c'_1}{p'_2}^{c'_2}\cdots {p'_{k'}}^{c'_{k'}}$。如果将指数对位相加,则可以得到 $ij$ 的质因数分解形式。
将每一个质因子 $p_l$ 枚举 $c_l+c'_l+1$ 次,便可以得到全部的因数。因此枚举 $x\mid i,y\mid j$,但是需要保证 $x,y$ 不包含重复质因子,即 $x\perp y$。
:::
## 迪利克雷卷积
对于数论函数 $f,g$,记 $h=f*g$ 为 $f,g$ 通过迪利克雷卷积相乘的到的结果,即:
$$
h(n)=\sum_{d\mid n}f(d)g\left(\dfrac nd\right)
$$
### 常见迪利克雷卷积
* $\varphi*1=\mathrm{id}$。
* $\mu*1=\varepsilon$。
* $\mu*1=\varphi$。
## 莫比乌斯函数
莫比乌斯函数 $\mu(n)$ 满足:
$$
\sum_{d\mid n}\mu(d)=[n=1]
$$
即 $\mu*1=\varepsilon$。
因此,$\mu(n)$ 可用于处理**互质**相关信息:
$$
[i\perp j]=[\gcd(i,j)=1]=\sum_{d\mid\gcd(i,j)}\mu(d)=\sum_{d\mid i\land d\mid j}\mu(d)
$$
### 莫比乌斯反演/莫比乌斯变换
若 $g(n)=\sum\limits_{d\mid n}f(d)$,则有:
$$
f(n)=\sum_{d\mid n}\mu(d)g\left(\dfrac nd\right)
$$
因为:
$$
\begin{aligned}
\sum_{d\mid n}\mu(d)g\left(\dfrac nd\right)&=\sum_{d\mid n}\mu(d)\sum_{d'\mid\large\frac nd}f(d')\\
&=\sum_{d\mid n}\mu(d)\sum_{d'\mid\large\frac nd}f(d')\\
&=\sum_{d\mid n}f(d)\sum_{d'\mid\large \frac nd}\mu(d')\\
&=\sum_{d\mid n}f(d)\left[n=d\right]\\
&=f(n)
\end{aligned}
$$
若 $g(n)=\sum\limits_{n|d}f(d)$,则有:
$$
f(n)=\sum_{n\mid d}\mu\left(\dfrac dn\right)g(d)
$$
> [[国家集训队] Crash的数字表格 / JZPTAB](https://www.luogu.com.cn/problem/P1829)
>
> [详见此处](https://www.cnblogs.com/TH911/p/-/P1829)。
莫比乌斯反演的本质其实就是高维差分。
## 线性筛
线性筛可以用于求解欧拉函数及莫比乌斯函数。实际上,**线性筛几乎可以求解所有的积性函数**,求解方法视具体函数而定。
### 欧拉函数
若 $i$ 为质数,显然 $\varphi(i)=i-1$。
设质数 $\textit{prime}_j$。
若 $i\bmod\textit{prime}_j\neq0$,则 $i\perp\textit{prime}_j$,因为 $\varphi$ 是一个积性函数,于是有:
$$
\varphi\left(i\cdot\textit{prime}_j\right)=\varphi(i)\cdot\varphi\left(\textit{prime}_j\right)
$$
否则,当 $i\equiv0\pmod{\textit{prime}_j}$ 时,有:
$$
\varphi\left(i\cdot\textit{prime}_j\right)=\varphi(i)\cdot\textit{prime}_j
$$
因为与 $i\cdot\textit{prime}_j$ 互质,即与 $i$ 互质。$1,2,3,\cdots,i\cdot\textit{prime}_j$ 在模 $i$ 意义下以 $i$ 为周期循环了 $\textit{prime}_j$ 次。
### 莫比乌斯函数
若 $i$ 为质数,显然 $\mu(i)=-1$。
设质数 $\textit{prime}_j$。
若 $i\bmod\textit{prime}_j\neq0$ 时,则 $i\perp\textit{prime}_j$,因为 $\mu$ 是一个积性函数,于是有:
$$
\mu\left(i\cdot\textit{prime}_j\right)=\mu(i)\cdot\mu\left(\textit{prime}_j\right)=-\mu(i)
$$
否则,当 $i\equiv0\pmod{\textit{prime}_j}$ 时,有:
$$
\mu\left(i\cdot\textit{prime}_j\right)=0
$$
因为 $i$ 含有 $\textit{prime}_j$,则 $i\cdot\textit{prime}_j$ 至少含有两个 $\textit{prime}_j$,故 $\mu\left(i\cdot\textit{prime}_j\right)=0$。
### 参考代码
```cpp
//ans1 为欧拉函数,ans2 为莫比乌斯函数
void pre(){
static int vis[N+1],prime[N+1],size;
ans1[1]=ans2[1]=1;
for(int i=2;i<=N;i++){
if(!vis[i]){
prime[++size]=i;
vis[i]=i;
ans1[i]=i-1;
ans2[i]=-1;
}
for(int j=1;j<=size&&i*prime[j]<=N;j++){
vis[i*prime[j]]=prime[j];
if(i%prime[j]==0){
//ans2 为 0,可以不写
ans1[i*prime[j]]=ans1[i]*prime[j];
break;
}
ans1[i*prime[j]]=ans1[i]*ans1[prime[j]];
ans2[i*prime[j]]=-ans2[i];
}
}
//前缀和,可不写
for(int i=1;i<=N;i++){
ans1[i]+=ans1[i-1];
ans2[i]+=ans2[i-1];
}
return;
}
```
## 杜教筛
杜教筛用于快速求解积性函数 $f$ 的前缀和 $S(n)=\sum\limits_{i=1}^nf(i)$。(实际上,只要能够构造恰当的函数,均可以使用杜教筛求解。)
构造一组 $h=f*g$,有:
$$
\begin{aligned}
\sum_{i=1}^nh(i)&=\sum_{i=1}^n\sum_{d\mid i}f\left(\dfrac id\right)g(d)\\
&=\sum_{d=1}^ng(d)\sum_{i=1}^{\lfloor\frac nd\rfloor}f\left(i\right)\\
&=\sum_{i=1}^ng(i)S\left(\left\lfloor\frac ni\right\rfloor\right)
\end{aligned}
$$
可得:
$$
\begin{aligned}
g(1)S(n)&=\sum_{i=1}^ng(i)S\left(\left\lfloor\dfrac ni\right\rfloor\right)-\sum_{i=2}^ng(i)S\left(\left\lfloor\dfrac ni\right\rfloor\right)\\
&=\sum_{i=1}^nh(i)-\sum_{i=2}^ng(i)S\left(\left\lfloor\dfrac ni\right\rfloor\right)
\end{aligned}
$$
因此,只要 $h,g$ 适当,能够快速地求出来,就能够在较短时间内求出 $S(n)$。
### 数论分块
可以发现,$\sum\limits_{i=2}^ng(i)S\left(\left\lfloor\dfrac ni\right\rfloor\right)$ 直接计算的复杂度较高,而 $\left\lfloor\dfrac ni\right\rfloor$ 就很适合用于数论分块来加速。
### 记忆化
每当我们求出了 $S(n)$ 之后,都应当将其记录下来,避免重复计算。
这样的杜教筛的时间复杂度时 $\mathcal O\left(n^{\frac34}\right)$ 的。
### 线性筛预处理
可以预处理较小的一部分的 $S(n)$,从而节约时间。
当预处理前 $\mathcal O\left(n^\frac23\right)$ 的 $S(n)$ 时,时间复杂度最小,为 $\mathcal O\left(n^\frac23\right)$。
#### 实际常数影响
在 OI 代码中,递归求解 $S(n)$ 会有常数的影响。
因此最好的方法应当是实际测试预处理部分大小 $m$ 的值从而确定时间最小的 $m$,这样即使复杂度劣一些,使用时间也小一些。
> [例题链接](https://www.luogu.com.cn/problem/P4213)
:::success[参考代码]
```cpp
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
#include<unordered_map>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>
using namespace std;
typedef long long ll;
__gnu_pbds::gp_hash_table<int,ll>mem1,mem2;
constexpr const int N=5e6/*1664510*/;
ll ans1[N+1],ans2[N+1];
ll S1(int n){
if(n<=N){
return ans1[n];
}
if(mem1[n]){
return mem1[n];
}
ll ans=n*(n+1ll)>>1;
for(ll l=2,r=n;l<=n;l=r+1,r=n){
ll t=n/l;
r=n/t;
ans-=(r-l+1ll)*S1(t);
}
return mem1[n]=ans;
}
ll S2(int n){
if(n<=N){
return ans2[n];
}
if(mem2[n]){
return mem2[n];
}
ll ans=1;
for(ll l=2,r=n;l<=n;l=r+1,r=n){
ll t=n/l;
r=n/t;
ans-=(r-l+1ll)*S2(t);
}
return mem2[n]=ans;
}
void pre(){
static int vis[N+1],prime[N+1],size;
ans1[1]=ans2[1]=1;
for(int i=2;i<=N;i++){
if(!vis[i]){
prime[++size]=i;
vis[i]=i;
ans1[i]=i-1;
ans2[i]=-1;
}
for(int j=1;j<=size&&i*prime[j]<=N;j++){
vis[i*prime[j]]=prime[j];
if(i%prime[j]==0){
ans1[i*prime[j]]=ans1[i]*prime[j];
break;
}
ans1[i*prime[j]]=ans1[i]*ans1[prime[j]];
ans2[i*prime[j]]=-ans2[i];
}
}
for(int i=1;i<=N;i++){
ans1[i]+=ans1[i-1];
ans2[i]+=ans2[i-1];
}
return;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
pre();
ll T;
cin>>T;
while(T--){
ll n;
cin>>n;
mem1.clear();
mem2.clear();
cout<<S1(n)<<' '<<S2(n)<<'\n';
}
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
```
:::