同余
模运算
一般的,对于任意整数 ,。则有:
性质:
- 。
简证:原式等价于 .
等号右边乘法分配律拆分,命题显然成立。
证毕。
证明:
原式可写为 .把下取整号内整数提出:.
消掉一部分:.
显然,等式成立。
证毕。
- 。
证明与上面一条差不多。
- 若 ,则有 。
证明:一定有 。
所以 。
而在模 意义下后面带 的项为 ,故得证。
同余
若 ,则称 与 在模 意义下同余,写作 。
定理:
-
。
-
若 ,那么 ,。
同余类与剩余系
对于 ,集合 中的所有数在模 意义下同余,余数均为 ,则称该集合为一个模 的同余类,简记 。
的同余类一共有 ,共 个,它们组成 的完全剩余系。
其中余数与 互质的一共有 个,其组成 的简化剩余系。
简化剩余系关于模 乘法封闭。
定理:
费马小定理:若 为质数,则 。
欧拉定理:若 ,则 。
证明:设 的简化剩余系为 。
对于任意 ,若 ,故 。
故 ,即 。
所以若 与 出自相同同余类,则 也出自相同同余类中,反之亦然。
由于简化剩余系运算关于模 乘法封闭,故 也属于简化剩余系。
所以集合 与 在模 意义下相等。
故 。
即 。
欧拉定理证毕。
若 为质数,则 ,代入原式费马小定理成立。
若 ,那么 只能是 的倍数,显然成立。
费马小定理证毕。
欧拉定理推论
推论1(降幂公式):
.
证明:若 显然成立。
否则,设 ,其中 。
所以:
证毕。
推论2:
若 ,则使得 成立的最小正整数 满足 。
证明:
反证法,假设最小正整数 满足 。
设 ,因为 ,所以 ,根据欧拉定理,,故 。
因为 ,所以 显然是满足条件的更小的正整数,与假设矛盾,故假设不成立,原命题成立。
证毕。
扩展欧拉定理
若 ,则有:
证明:
前者显然成立,主要证明 的情况。
若 ,则根据推论 可轻松得证。
否则,设 的质因数分解为 。
引理: 成立是 成立的充要条件。
证明:显然 恒成立。
考虑对于两个同余式 ,若 ,则有 。
证明很简单,移项有 ,所以有 即 ,再次移项即可证明,必要性也可以这么证。
通过上述定理,显然可以将若干模数两两互质且两个同余号左右内容一致的同余方程合并成一个。故引理成立。
证毕。
若 ,则根据降幂公式,有 ,所以
若 ,则一定有 ,记 。
引理:.
证明:显然前两个不等号成立,这里主要证明第三个。
因为 ,所以 越大时, 越大,而 不变,故只考虑 时即可。
考虑分讨, 时 成立, 时 成立。
时,有 恒成立。综上引理成立,证毕。
所以 ,综合引理有 。
同理,。
故 。
综上所述,扩展欧拉定理成立。
证毕。
例1.上帝与集合的正确用法
其实就是求 :
根据扩展欧拉定理,有:
所以只需求:
发现可以递归求。
引理: 收敛到 ,至多在外面套上 个 。
证明:首先根据 的计算式,,如果 或者 显然是奇数,
如果 是偶数,则一定有以下条件成立其中一个:
是奇数。
的质因数分解中,质因子 的次幂 。
很显然,上述两个条件成立其中一个必然会让 乘上一个偶数,从而使得整个值一定为偶数。
显然,除了 其余所有正整数均满足上述两个条件。
所以 ,。
而又因为如果 是偶数,则比 小的所有偶数都不与 互质,故 。
综合上述两点,命题成立,证毕。
const int N=1e7+10;
int t,Mo;
signed phi[N],v[N];
vector<signed>prime;
int power(int a,int b,int p) {
int res=1;
for(;b;b>>=1) {
if(b&1) res=res*a%p;
a=a*a%p;
}
return res%p;
}
void Euler(int n) {
FOR(i,1,n) phi[i]=i;
for(int i=2;i<=n;++i) {
if(!v[i]) v[i]=i,prime.pb(i),phi[i]=i-1;
for(int p:prime) {
if(p*i>n||v[i]<p) break;
v[i*p]=p;
phi[p*i]=phi[i]*(i%p?p-1:p);
}
}
return ;
}
int dfs(int x) {
if(x==1) return 0ll;
return power(2,dfs(phi[x])+phi[x],x);
}
void solve() {
cin>>Mo;
cout<<dfs(Mo)<<"\n";
}
main() {
cin>>t;
Euler(N-10);
while(t--) solve();
return 0;
}
模意义下的除法
由于除法在模意义下不一定满足 若 等价于 ,所以要找到一种办法去求除法取模意义下的值。
逆元:一般的,对于一个数 ,若存在 使得同余方程 成立,则称 为 在模 意义下的逆元,记为 。
引理: 存在模 意义下的逆元,当且仅当 。
证明:反证法,假设 。设
。
又因为 ,且 是 的倍数,故 不可能为 。故假设不成立,原命题成立。
证毕。
费马小定理求逆元
若 为质数,则求 在模 意义下的逆元:
根据费马小定理,,所以 。
将同余号左写成 的形式。显然,。
所以 是方程 的一组解。
所以快速幂直接求 即可,时间复杂度 。
欧拉定理求逆元
与费马小定理几乎一致。
因为要求解 的欧拉函数值,所以时间复杂度为 ,显然不是很优秀。
但此方法可以求任意一对 的逆元(当然要满足 )。
线性递推 的所有逆元
第一种方法:
显然 。
考虑当前要求 。
显然有 。
即 。
令 。
同时乘上 。
而由于 ,所以只要把 的逆元全部求出就一定可以求出 的逆元。
但是,当 为合数时或者 时,有可能 ,此时 , 一定不存在小学老师告诉过你 能做除数吗。
所以线性递推逆元的方法(包括下面的阶乘递推逆元)都只适用于 且 为质数的情况。
第二种方法:
预处理出 所有数的阶乘在 意义下的值。
显然 。
这样就可以预处理出所有 阶乘的逆元。
由于 。
所以也可以线性求。
一般用第二种方法,因为第二种方法会顺便求出阶乘以及阶乘逆元,这在求组合数中十分有用处。
裴蜀定理(Bézout定理)
对于任意整数 ,存在整数 ,使得 。
证明:
在欧几里得求 的最后一步,即 时,一定存在一组解 ,使得 。
假设存在一组解 使得 。
根据欧几里得算法,。
又因为:
则此时令 ,就得到了使得 成立的一组解。
对此使用数学归纳法,命题得证。
该证明同时还给出了整数 的计算方法,我们称其为扩展欧几里得算法。
void Exgcd(int a,int b,int &x,int &y) {
if(!b) return x=1,y=0,void();
Exgcd(b,a%b,x,y);
int t=x;x=y;y=t-(a/b)*y;
}
对于更为一般的情况,即 ,该方程有解当且仅当 。
证明:
设 。
则:
显然左侧为整数,故右侧也应为整数,即 。
证毕。
先用扩展欧几里得算法求出一组方程 的特解,将求出的特解 同时乘上 即可求出原方程的一组特解。
原方程通解即:
证明:
- 当 互质。
则原式可写为 。
若要满足整数的条件,则必须满足 。
显然, 不变,变的数字只有同余号右侧,假设改变后的数字是 。
而 改变只会让 变成 的倍数,所以有 。
又因为只有当 时, 在模 意义下的值等于 。所以有 。
所以改变后的数字可以被写成 。
此时 增长了 。
同时解出对应的 ,增长了 。
而 可以取任意正整数。
- 当 不互质。
不互质则可以通过将原方程 同时除以 得到,解集显然不变。
证毕。
例2. [NOIP2012 提高组] 同余方程
考虑对原式进行变形:
换元,令 :
这是什么?不定方程!
它有解当且仅当 。
直接用扩展欧几里得算法求出一组特解,再通过取模运算求得最小正整数解。
时间复杂度 。
LL a,b;
/*
ax=1(mod b)
b|ax-1
ax+by=1
*/
int exgcd(LL a,LL b,LL &x,LL &y) {
if(!b) return x=1,y=6+1,a;
int d=exgcd(b,a%b,x,y);
LL t=x;x=y;y=t-(a/b)*y;
return d;
}
main() {
cin>>a>>b;
LL x,y;int d=exgcd(a,b,x,y);
cout<<(x%b+b)%b<<"\n";
return 0;
}
中国剩余定理(CRT)与线性同余方程组
设 是两两互质的整数,设 为 在模 意义下的逆元(可取任意解)。
对于任意 个整数 方程组:
有整数解,解为 。
证明:
考虑 的特解 。
由于 ,且 。
故 。
又因为模数两两互质可以合并,故得证。
中国剩余定理给出了模数两两互质的方程组的一组通解。
一般的,使用中国剩余定理求解线性方程组的时间复杂度为 ,其中 为值域。
(其实,即使模数不满足两两互质,我们也有方法求解方程组,并有着与上述同样优秀的时间复杂度)
例3.曹冲养猪
自信点,把题目中“假定”去掉。
直接中国剩余定理即可。
const int N=11;
int n,a[N],c[N];
int exgcd(int a,int b,int &x,int &y) {
if(!b) return x=1,y=6+1,a;
int d=exgcd(b,a%b,x,y);
int z=x;x=y;y=z-(a/b)*y;
return d;
}
int mul(int x,int y,int p) {
int ans=0;
for(;y;y>>=1) {
if(y&1) ans=(ans+x)%p;
x=(x+x)%p;
}
return ans%p;
}
int CRT() {
int M=1ll,ans=0;
FOR(i,1,n) M*=a[i];
FOR(i,1,n) {
int MI=M/a[i],b=a[i],x,y;
int d=exgcd(MI,b,x,y);
x=(x%a[i]+a[i])%a[i];
ans=(ans+mul(mul(x,c[i],M),MI,M))%M;
}
return (ans+M)%M;
}
main() {
cin>>n;
FOR(i,1,n) cin>>a[i]>>c[i];
cout<<CRT()<<"\n";
return 0;
}
更加一般的情况
现在我们考虑更加一般的情况。
例4.扩展中国剩余定理(EXCRT)
问题:求解上述线性方程组,但不保证 两两互质。
考虑使用数学归纳法,不断合并方程组,最后归为一个方程并求出答案。
先考虑将两个方程合并成一个:
转化:
故:
换元,令 。
显然可以扩欧,若没有解则原方程组无解。
求出 的一组特解,将其中一个代入原式即可求出一个同时满足两个方程的特解 :
考虑求出通解,发现要满足原来的式子加减的数字只能同时是 和 的倍数。
覆盖的最大范围显然是最小公倍数。
综上,我们求得了一个合并后的方程组:
合并 次后即可求解。
const int N=1e5+10;
int mul(int a,int b,int p) {
int ans=0;
for(;b;b>>=1) {
if(b&1) ans=(ans+a)%p;
a=(a+a)%p;
}
return ans%p;
}
int exgcd(int a,int b,int &x,int &y) {
if(!b) return x=1,y=6+1,a;
int d=exgcd(b,a%b,x,y);
int z=x;x=y;y=z-(a/b)*y;
return d;
}
int n,r[N],m[N];
int EXCRT() {
int M=m[1],R=r[1],d,x,y;
FOR(i,2,n) {
int c=((r[i]-R)%m[i]+m[i])%m[i];
d=exgcd(M,m[i],x,y);
if(c%d) return -1;
x=mul(x,c/d,m[i]/d);
R+=x*M;
M/=d;M*=m[i];
R=(R%M+M)%M;
}
return (R%M+M)%M;
}
main() {
cin>>n;
FOR(i,1,n) scanf("%lld%lld",&m[i],&r[i]);
cout<<EXCRT()<<"\n";
return 0;
}
综合应用
例5.abc193_e Oversleeping
设公交循环了 次 “从 A 出发到 B,在 B 停 y 秒后从 B 出发到 A,在 A 停 y 秒”,此人重复睡去醒来的次数为 ,公交车已经停在 B 地 秒,此人醒来 秒下车。
则有:
显然这是一个不定方程的形式。
枚举 ,使用扩展欧几里得求出一组 的最小正整数解并带入原式更新答案即可。
复杂度 。
例6.[六省联考 2017] 相逢是问候
暴力显然不可做。
根据例1,一个数字最多更新 次在模 意义下的值就不会变化了。
所以可以用一个势能分析线段树维护。
const int N=5e4+10,M=55;
int a[N],n,m,P,c,tot,use[N],pos[M],cp[M][N],cp1[M][N];
bool b[M][N],b1[M][N],flg;
/*
a i表示原数列。
use i表示 a i 做了多少次操作。
pos i表示p做 i 次欧拉函数的值。
cp ij表示 c^j 对 pos i 取Mod的值。 (b)
cp1 ij表示 c^(10000j) 对 pos i 取Mod的值。(b1)
*/
int Phi(int x) {
int res=x,cnt=sqrt(x);
FOR(i,2,cnt) {
if(x%i) continue;
while(x%i==0) x/=i;
res/=i;res*=(i-1);
}
if(x>1) res/=x,res*=(x-1);
return res;
}
struct SegmentTree {
int l,r,sum;
bool all;
}t[N<<2];
void Pre() {
FOR(i,0,tot) {
cp[i][0]=1;
FOR(j,1,10000) {
cp[i][j]=cp[i][j-1]*c;
if(cp[i][j]>=pos[i]) b[i][j]=1,cp[i][j]%=pos[i];
b[i][j]|=b[i][j-1];
}
}
FOR(i,0,tot) {
cp1[i][0]=1;
b1[i][1]=b[i][10000];
FOR(j,1,10000) {
cp1[i][j]=cp1[i][j-1]*cp[i][10000];
if(cp1[i][j]>=pos[i]) b1[i][j]=1,cp1[i][j]%=pos[i];
b1[i][j]|=b1[i][j-1];
}
}
}
int power(int p,int ind) {
int p1=p/10000,p2=p%10000;
int res=cp[ind][p2]*cp1[ind][p1];
if(res>=pos[ind]) flg=1,res%=pos[ind];
flg|=b[ind][p2]|b1[ind][p1];
return res;
}
int dfs(int x,int dep,int ind) {
flg=0;
if(dep==ind) {
if(x>=pos[ind]) flg=1,x%=pos[ind];
return x;
}
int y=dfs(x,dep+1,ind);
return power(y+flg*pos[dep+1],dep);
}
void push_up(int p) {
t[p].all=t[p<<1].all;t[p].all&=t[p<<1|1].all;
t[p].sum=(t[p<<1].sum+t[p<<1|1].sum)%P;
}
void Build(int p,int l,int r) {
t[p].l=l,t[p].r=r;
if(l==r) return (void)(t[p].sum=a[l],t[p].all=use[l]=0);
int mid=l+r>>1;
Build(p<<1,l,mid);
Build(p<<1|1,mid+1,r);
push_up(p);
}
void change(int p,int l,int r) {
if(t[p].all) return ;
if(t[p].l==t[p].r) {
use[t[p].l]++;
if(use[t[p].l]==tot) t[p].all=1;
t[p].sum=dfs(a[t[p].l],0,use[t[p].l]);
return ;
}
int mid=t[p].l+t[p].r>>1;
if(l<=mid) change(p<<1,l,r);
if(r>mid) change(p<<1|1,l,r);
push_up(p);
}
int ask(int p,int l,int r) {
if(l<=t[p].l&&t[p].r<=r) return t[p].sum;
int mid=t[p].l+t[p].r>>1,val=0;
if(l<=mid) val+=ask(p<<1,l,r);
if(r>mid) val+=ask(p<<1|1,l,r);
return val%P;
}
main() {
cin>>n>>m>>P>>c;
FOR(i,1,n) a[i]=read();
pos[0]=P;
while(pos[tot]!=1) ++tot,pos[tot]=Phi(pos[tot-1]);
pos[++tot]=1;
Pre();
Build(1,1,n);
while(m--) {
int op=read(),l=read(),r=read();
if(!op) change(1,l,r);
else cout<<ask(1,l,r)%P<<"\n";
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具