专栏文章
数论学习笔记
算法·理论参与者 11已保存评论 12
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 12 条
- 当前快照
- 1 份
- 快照标识符
- @mio8zaiy
- 此快照首次捕获于
- 2025/12/02 15:19 3 个月前
- 此快照最后确认于
- 2025/12/02 15:19 3 个月前
本文可能会不定期更新。若存在内容正确性上的问题,请联系修复。
更新/修正记录
- 更新:修正 excrt 的参考代码。
- 更新:修正杜教筛的推导过程。
本文中出现的代数式、数,如无特殊说明,均为整数。
对于 OI 中的数论题,答案不对请先尝试:
CPP#define int __int128
typedef long long ll;
#define ll __int128
数论函数指的是定义域为正整数的函数,也可以视为一个序列。
基础部分
在数论中表示“互质”,例如 。
对于一个序列 ,若 ,则有:
质数
Fermat 素性测试
由费马小定理,对于质数 ,若 ,满足 ,。
但是 并不能推出 为质数。
因此可以随机选择来测试。
Miller–Rabin 素性测试
Miller-Rabin 素性测试,取质数集合 ,则可以通过随机化确定
long long 范围()内的任意整数 是否为质数。从 中取一底数 ,若:
- , 为质数。
- , 不为质数。
在都不成立的情况下,进行 Miller-Rabin 素性测试。
将 转化:
其中,,也就是说, 是 在二进制位上的一个前缀,满足前缀的后缀不为 。
二次探测定理
使用平方差公式可证。
Miller-Rabin 素性测试的实现
基于 执行 轮测试,通过二次探测定理判断。
参考代码
CPPconstexpr 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;
}
最大公约数
裴蜀定理
对于 :
-
,使 。
-
,。
逆定理
,若 且 为 的公因数,且存在 ,使得 ,则:
乘法逆元
若存在 ,则称 为 关于 的逆元,记 。
当 的时候( 为质数)逆元一定存在。
费马小定理
对于质数 ,若 ,满足 ,。
常用于分数取模,即:
详见费马小定理。
欧拉定理
若 ,则 。
显然,当 为质数时,有 ,即费马小定理。因此,费马小定理为欧拉定理的特殊形式。
扩展欧拉定理
对于任意的 ,若满足 ,有:
当 时即欧拉定理。
可以使用初等证明或群论证明。
常用于降幂。
扩展欧几里得算法 exgcd
注意不是“类欧几里得算法”。
用于求关于 的不定方程 的一组整数特解。
不妨令 。
考虑到 ,代入可得:
因此:
令 ,有:
显然,可以递归求解。
那么直到 时,可以直接解得一组整数特解 。
设最终递归出来的解为 。
令 ,则对于 ,方程存在一组解为 。
参考代码
CPPvoid 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 求乘法逆元
如果需要讨论 模 意义下的乘法逆元 ,显然有 。
那么可列方程 ,显然 。
则通过扩展欧几里得算法求出 ,即求出了 的逆元。
求出 后,为了使 为正整数,只需要让 即可。
参考代码
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;
}
例题:正整数最大/小解
给定不定方程 ,,判断其是否有解。若有正整数解,分别求 的最大/小值。否则,求 得最小正整数解。
对于无解,即 ,很好判断。
先通过 exgcd 求出一组特解 ,则 也是原方程的解。 为 的单次变化量,最小为 ,。
先求 的最小正整数解 ,即 最小,显然 。
但是为了保证 为正整数,因此当 时,。
,因此 最小时 最大,因此可以确定 。
同理,可求出 。
当 为正整数,此时若 ,则存在正整数解 ,因此可以判断正整数解(判断 也行)。
正整数解的个数即:
因为从 开始, 对应一组 。
参考代码
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 求解的是不定方程:
而同余方程形如:
可以将同余方程转化为:
进而使用 exgcd 求解。
线性求逆元
有一种方法,可以在 的时间内求出任意 个数 关于 的逆元。
记:
那么就可以 计算:
递推,可以推出:
随后再次递推,可以得到 关于 的逆元 :
参考代码
CPPpre[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 中国剩余定理
CRT 中的运算溢出
在使用 CRT 或 exCRT 时,一律建议使用
__int128,防止溢出。尽管题目大多会保证 ,但是中间计算时会溢出。
CRT(中国剩余定理)被用于求解线性同余方程组:
其中, 两两互质。
令 ,则对于 有 。
记 ,设 ,即 模 意义下的逆元。
则,方程组的最小整数解为:
同时,对于 , 均为原方程组的解。
CRT 其实就是一个构造式的做法,易证 。
例题:中国剩余定理(CRT)/ 曹冲养猪
题目中的 即分别为 。
直接套 CRT 即可,但是需要注意的是,计算 时需要
__int128,会爆 long long。参考代码
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
exCRT 实际上与 CRT 关系不大,如同 exLucas 与 Lucas 一般。
exCRT 解决的是当模数 不一定两两互质的时候(这时有可能无解)。
先考虑两个同余方程:
设 ,将其转化为两个不定方程:
因此可得 。
由裴蜀定理,若 ,则该不定方程无解,则原同余方程组无解。
否则通过 exgcd 求出 ,则:
既然 ,因此有:
这样取 是为了将两个方程合并。
这样,两两顺次合并,最终合并为一个方程即可解。
参考代码
CPPfor(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';
}
}
参考代码
并没有判断无解。
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
即形如:
多了一个系数 。
同样可以用类似 exCRT 的方法两两合并解决,具体见此。
离散对数
所谓离散对数,即模意义下取对数。
形如:给定模数 ,及整数 ,求整数 使得 。
注意:离散对数可能不存在。
BSGS 算法
在 OI 中,BSGS 算法(Baby-Step Giant-Step,大步小步算法北上广深算法)常用来求解离散对数。
BSGS 算法要求 ,求 使得 。
若有解,则存在 的解;因为欧拉定理说明, 时,。
若枚举 是 的,当 为质数的时候不能接受,因此可以考虑分块。
令 ,,且 。
则有:
,则 的乘法逆元 一定存在,有:
枚举 ,将其与其对应的 一同存储在数据结构(一般是哈希表)中,随后枚举 ,在数据结构中找 对应的 ,找到了 便找到了一个解 。同时,因为 ,因此 模 意义下互不相同。
时间复杂度:,在 为质数时最劣,。
参考代码
CPPB=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";
参考代码
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 可解决 不成立的情况。
由扩展欧拉定理可知,若 有解,则 范围内也有解。
首先特判掉 的情况,即 或 。
考虑分块,令 ,则有:
因此可以预处理 ,随后枚举 ;但是注意到前者是后者的充分条件而不是充要条件,因此找出来的 只是“可能的解”,需要检验。
对于多个不同的 产生的模 意义下相同的 ,可以只存储 最小的两个。
证明
由扩展欧拉定理, 是在 值后以 为周期循环的。
循环周期内的 显然至多存储 个,而非循环部分 也至多存储 个。
即:保留最小的 个。
时间复杂度:仍然为 。
参考代码
CPPa%=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";
}
参考代码
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 定理的内容很简单:
考虑如何证明。
证明
由二项式定理:
考虑 在模 意义下的取值。
因为:
那么如果化简之后,分子 中的 项没有被约分掉,则有 。
因为 为质数,所以 项能被约分掉当且仅当 中含有 项或 中含有 项。
即: 当且仅当 或 。
在二项式定理中, 满足 ,所以 当且仅当 或 。
在这两种情况中,都可以计算得到 ,即 。
重新带回二项式定理,可得:
考虑一个二项式 。
由二项式定理, 的 项系数为 。
而想要从 中得到 ,即从 中选取 个 ,再从 中选取 个 。
即:
你可能有一个问题:这看起来明明应当是一个等式,但为什么是同余呢?
即,Lucas 定理应当表述为:
但是,你要知道,只有在模 意义下才有 。
因此,只有在模 意义下才有:
即:
应用
当 比较大而无法使用其他方法(例如预处理 的阶乘再利用乘法逆元)直接求解组合数时,可以使用 Lucas 定理。
Lucas 定理只需要递归使用即可,即递归计算 ,递归终点即 。
其时间复杂度为:。
其中, 表示单次计算 的复杂度,因写法而异。
可以使用乘法逆元,则时间复杂度为 。
也可以 递推,时间复杂度为 。
推荐使用乘法逆元。
很简单,注意是 即可。
参考代码
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 算法可以求 , 不一定为质数。(但是,exLucas 实际上和 Lucas 没有多大关系……)
由唯一分解定理,可以对 进行质因数分解:
CRT 求解
我们如果能够求出 使得:
那么我们就可以通过 CRT 求解出 。因为在 CRT 中,恰好也有 。
求解模质数幂下余数
展开定义式:
因此:
因为 不一定互质,因此乘法逆元不一定存在。
考虑先提出分子和分母中所有的 次幂,随后便可以使用逆元求解。
记 的质因数分解中质数 的幂次为 ,剩余积为 ,即:
则有:
那么,现在只需要考虑对于 ,如何高效地求出 。
考虑:
容易发现,在 中 的个数为 。其中 是 中的 的个数。
为质数,所以在 中不会含有 。
将递推式展开:
因此可以 计算 :
CPPint v(ll n,ll p){
int ans=0;
do{
n/=p;
ans+=n;
}while(n);
return ans;//这里取不取模其实都无所谓,因为幂次不会太多,想取也可以.
}
现在考虑如何计算 ;显然不能利用定义式 ,而需要其他方法(因为无法得知 )。
不难进行递推:
由 Wilson 定理的推论( 时 ,否则为 ):
因此:
可以发现,每次计算 时, 是固定的,因此可以预处理:
因此,得到最终推导式:
可以递归或迭代处理,时间复杂度 。
CPPint 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;
}
参考代码
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
*/
参考代码
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;
}
数论分块
数论分块(整除分块)用于快速计算:
数论分块的原理即本质不同的 只有至多 种。
证明
- 当 时, 只有 种取值。
- 当 时,, 只有 种取值。
故,本质不同的 只有至多 种取值。
因此 相同值肯定是连续的,那么就可以找出这 区间 (),求出:
将 个答案相加即可。
若分块后可以 求出,则数论分块是 的。
寻找区间
对于一个 所对应的 ,要寻找一个最大的 使得 ,从而找到区间 。
则有:
而对于向上取整的情形,即对于 对应的 ,要寻找一个最大的 使得 ,从而找到区间 。
则有:
单一参数
给定 ,满足 ,求:
显然,。
因此有:
考虑快速求出 ,可以进行数论分块。
对于数论分块中一组确定的 ,有:
这可以 计算,因此总计算时间复杂度为 。
参考代码
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;
}
多参数
此时寻找 ,会取 ,即:
给定 ,求:
不妨令 ,否则交换 。
模运算不好处理,转换一下:
不好处理,因此可以先全部求出来,再减去相等的部分:
不难发现 与 的取值无关,因此转换为:
于是可以 计算,但是考虑到 ,只需要使用数论分块优化即可。
参考代码
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;
}
积性函数
定义
若数论函数 满足 ,且对于 ,有 ,称 为积性函数。
若数论函数 满足 ,且对于 ,有 ,称 为完全积性函数。
基础部分
若 均为积性函数,则 均为积性函数。其中 为迪利克雷卷积:
常见积性函数
-
单位函数:,完全积性函数。(
\varepsilon) -
恒等函数:, 通常记为 ,完全积性函数。
-
常数函数:,完全积性函数。
-
除数函数:, 通常记作 或 , 通常记作 。(
\sigma)的求法
令 ,其中 均为质数。对于每一个指数 的更改,都会产生新的因数。每一个 ,都有 种选择。显然有:时间复杂度 。 -
欧拉函数:。(
\varphi) -
莫比乌斯函数:即 含有平方因子。(
\mu)
同时,除数函数 即 的因数个数,且 满足:
证明
令 。如果将指数对位相加,则可以得到 的质因数分解形式。
将每一个质因子 枚举 次,便可以得到全部的因数。因此枚举 ,但是需要保证 不包含重复质因子,即 。
迪利克雷卷积
对于数论函数 ,记 为 通过迪利克雷卷积相乘的到的结果,即:
常见迪利克雷卷积
- 。
- 。
- 。
莫比乌斯函数
莫比乌斯函数 满足:
即 。
因此, 可用于处理互质相关信息:
莫比乌斯反演/莫比乌斯变换
若 ,则有:
因为:
若 ,则有:
详见此处。
莫比乌斯反演的本质其实就是高维差分。
线性筛
线性筛可以用于求解欧拉函数及莫比乌斯函数。实际上,线性筛几乎可以求解所有的积性函数,求解方法视具体函数而定。
欧拉函数
若 为质数,显然 。
设质数 。
若 ,则 ,因为 是一个积性函数,于是有:
否则,当 时,有:
因为与 互质,即与 互质。 在模 意义下以 为周期循环了 次。
莫比乌斯函数
若 为质数,显然 。
设质数 。
若 时,则 ,因为 是一个积性函数,于是有:
否则,当 时,有:
因为 含有 ,则 至少含有两个 ,故 。
参考代码
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;
}
杜教筛
杜教筛用于快速求解积性函数 的前缀和 。(实际上,只要能够构造恰当的函数,均可以使用杜教筛求解。)
构造一组 ,有:
可得:
因此,只要 适当,能够快速地求出来,就能够在较短时间内求出 。
数论分块
可以发现, 直接计算的复杂度较高,而 就很适合用于数论分块来加速。
记忆化
每当我们求出了 之后,都应当将其记录下来,避免重复计算。
这样的杜教筛的时间复杂度时 的。
线性筛预处理
可以预处理较小的一部分的 ,从而节约时间。
当预处理前 的 时,时间复杂度最小,为 。
实际常数影响
在 OI 代码中,递归求解 会有常数的影响。
因此最好的方法应当是实际测试预处理部分大小 的值从而确定时间最小的 ,这样即使复杂度劣一些,使用时间也小一些。
参考代码
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;
}
相关推荐
评论
共 12 条评论,欢迎与作者交流。
正在加载评论...