高次同余方程,二次同余方程学习笔记
写在前面
文章作者实力有限,本文可能有个别错误,如有错误请友好地指出。
高次同余方程就是xa≡b(mod p)xa≡b(mod p)
二次同余方程就是x2≡b(mod p)
我们接下来讨论解这两种方程的方法。
那么有一个问题。既然知道了高次同余方程的解法,就可以直接用解高次同余的方法解二次剩余方程。为什么要单独学二次同余方程呢。
因为我区间加区间修改用的是线段树不是树套树。即问题特殊化之后可以使用一些特殊的方法,这种方法可能会比一般方法高效,简便。
正文
高次同余方程
首先需要知道阶和原根
阶
定义
满足ax≡1(mod p)的最小的正整数x是a关于模p的阶,接下来a的阶表示为<a>。
条件
gcd(a,p)=1,这是显然的。
性质
1:<a>∣φ(p)。
证明:
因为aφ(p)≡1(mod p),a<a>≡1(mod p)
所以aφ(p)−<a>≡1(mod p)
进而得出aφ(p) mod <a>≡1(mod p)
假设<a>不是φ(p)的约数。
φ(p) mod<a>≠0
(φ(p) mod<a>) < (<a>)与<a>的最小性矛盾,与假设矛盾原命题成立。
证毕。
2:设a关于模p的阶为<a>则a0,a1,a2,...,a<a>−1两两不同。
证明:
假设ax≡1(mod p),ay≡1(mod p),且ax≡ay(mod p),x<y<<a>
则ay−x≡1(mod p)。y−x<<a>与阶的最小性矛盾,假设不成立,原命题成立。
证毕。
3:设a关于模p的阶为<a>,则ax≡ay(mod p)的充要条件是x≡y(mod <a>)。
两边一直除a<a>因为a<a>≡1(mod p)所以结论显然。
原根
(因为原根的定义涉及到阶,所以默认互质,又因为原根的性质3,p为质数)
定义
满足g关于模p的阶等于φ(p)的g是p的一个原根。
性质
1:设g是模p的一个原根,那么对于任意一个小于p的正整数x,都存在唯一一个值r使得r是以g为底的x对模p的指数。
其实就是说gr≡x(mod p)的r是唯一的,反过来也一样。其中g为p的一个原根,x<p,r<φ(p)其实就是阶的第二个性质。不再赘述。
2:对于一个质数,所拥有的原根个数是φ(φ(p))。
3:一个模数存在原根的从要条件是,这个模数可以表示为1,2,4,p,2p,pn其中p是奇质数,n是任意正整数
(这两个性质的证明可能会用到抽象代数的知识,我不会555...)
那么如何找原根?
考虑到最小的原根一般比较小,所以我们枚举,原根然后判断。
假设我们当前枚举到i,判断的时候只需要枚举φ(p)的质因子p1,p2,p3...pk。然后判断iφ(p)pj是不是全部都不是1,如果全部都不是1,i就是p的一个原根。
为什么?
我们这个过程其实是枚举φ(p)的约数,因为阶的性质1,所以i的阶是φ(p)的一个约数,我们只需要看有没有一个φ(p)的约数x使得ix≡1(mod p)就能判断i是不是原根了。
那为什么不直接枚举约数,用这种枚举方法对吗?
φ(p)的约数至少比φ(p)缺少一个质因子,因为如果xa≡1那么xab≡1,所以少多个质因子的情况被少一个质因子的情况包含,所以枚举所有少掉一个质因子的情况即φ(p)pj就行。
解高次同余方程的步骤
首先要会BSGS和exgcd
xa≡b(mod p)
1:先求出p的一个原根g。
2:求出以g为底的b关于模p的指数r,即gr≡b(mod p)(离散对数,用BSGS求解)
3:令x≡gy(mod p),带回原式就是求gay≡gr(mod p)
4:根据阶的性质3就是求ay≡r(mod p)(一次同余方程用exgcd求解)
5:在0~φ(p)−1中求得y的解,带回x≡gy(mod p)解完方程。
有些值得注意的点:
因为原根的性质1求出的答案没有重复。
为什么要用原根代换b,别的数不行吗?我们需要用原根表示b。见原根的性质1。
因为要求原根且p为质数所以试用的p的范围很小,见原根的性质3。
只要求求出通解的复杂度为O(√p)
下面是输出所有解的代码。。。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=201010;
unordered_map<int,int> hsh;
int prime[N],tot,T,phi,ans[N],num,a,b,p,g,x,y;
int read(){
int sum=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){sum=sum*10+ch-'0';ch=getchar();}
return sum*f;
}
void work(int x){
int tmp=x;
tot=0;
for(int i=2;i*i<=x;i++){
if(tmp%i==0){
prime[++tot]=i;
while(tmp%i==0)tmp/=i;
}
}
if(tmp>1)prime[++tot]=tmp;
}
int ksm(int x,int b){
int tmp=1;
while(b){
if(b&1)tmp=tmp*x%p;
b>>=1;
x=x*x%p;
}
return tmp;
}
int BSGS(int a,int b){
hsh.clear();
int block=sqrt(p)+1;
int tmp=b;
for(int i=0;i<block;i++,tmp=tmp*a%p)hsh[tmp]=i;
a=ksm(a,block);
tmp=1;
for(int i=0;i<=block;i++,tmp=tmp*a%p){
if(hsh.count(tmp)&&i*block-hsh[tmp]>=0)return i*block-hsh[tmp];
}
}
int exgcd(int &x,int &y,int a,int b){
if(b==0){
x=1;y=0;
return a;
}
int gcd=exgcd(x,y,b,a%b);
int z=x;
x=y;y=z-(a/b)*y;
return gcd;
}
signed main(){
T=read();
while(T--){
p=read(),a=read(),b=read();
b%=p;
phi=p-1;
work(phi);
for(int i=1;i<=p;i++){
bool flag=false;
for(int j=1;j<=tot;j++)if(ksm(i,phi/prime[j])==1){flag=true;break;}
if(flag==false){g=i;break;}
}
int r=BSGS(g,b);
int gcd=exgcd(x,y,a,phi);
if(r%gcd!=0){printf("No Solution\n");continue;}
x=x*r/gcd;
int k=phi/gcd;
x=(x%k+k)%k;
num=0;
while(x<phi){ans[++num]=ksm(g,x),x+=k;}
sort(ans+1,ans+1+num);
for(int i=1;i<=num;i++)printf("%lld ",ans[i]);
printf("\n");
}
return 0;
}
二次同余方程
(p为素质数至于为什么要有素,接着看就知道了)
x2≡A(mod p)
一些引理
引理1:x有正整数解的充要条件是Ap−12≡1(mod p)
证明:
由费马小定理得
设x2≡A(mod p)则Ap−12≡xp−1(mod p)
由费马小定理
证毕。
推论:x无正整数解的时Ap−12≡−1(mod p)
引理2:一共只有(p−1)/2(不考虑0)个数存在模p意义下的二次方根,对于其中的每个数都存在两个互为相反数的解。
证明:
假设有两个数u,v,u2−v2≡0(mod p)那么(u+v)∗(u−v)≡0(mod p),因为u,v⊂[1,p−1](不考虑0),所以−p+1≤u−v≤p−1无法被p整除,所以u+v≡0(mod p)。
所以u,v互为相反数且对应唯一一个A,反过来也一样。因为不考虑0且p为奇数,这样的数有p−12对,且对应着p−12个A。
证毕。
此时我们就可以有一个解法。
解法一
1:我们求出p的一个原根g。然后求出以g为底的A关于模p的指数r,即gr≡A(mod p)(离散对数,用BSGS求解)
2:A满足Ap−12≡1(mod p)带入得gr(p−1)2≡1(mod p)由原根的定义得p−1∣r(p−1)2⇒r∣2。然后gr2就是一个解,因为r∣2所以可以直接快速幂,另一个解就是相反数。
又一些引理
定义:如果x2≡n(mod p)有解就说n是模p意义下的一个二次剩余,无解就说n是模p意义下的一个非二次剩余。
然后就是一个非常神的推导。
我们定义设a满足w=a2−n是一个非二次剩余。
引理3:(a+√w)p≡a−√w(mod p)这实际上拓展了数域,可以用复数去理解。
证明:
把(a+√w)p用二项式定理展开为∑pj=0Cjpajwp−j。因为p是一个质数所以当j!=0且j!=p时Cjp都为0否则为1,因为Cjp分子中有p消不掉最后mod p得0。那么(a+√w)p≡ap+√wp≡ap−1∗a+wp−12∗√w(mod p)由费马小定理和引理1的推论(别忘了w是一个非二次剩余)最后化简为a−√w。
推导
然后我们随机一个a满足条件,x≡(a+√w)p+12(mod p)根据拉格朗日定理可得答案中w的系数必然为0(然而我并不知道拉格朗日定理,所以模板拓展数域的乘法是背的),就可以解得x了,又因为引理2所以期望随机次数为2。复杂度可以保证。
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstdlib>
#include<ctime>
using namespace std;
#define random(a,b) (rand()%(b-a+1)+a)
#define int long long
int n,p,w;
bool flag;
struct complex{
int x,y;
complex (int xx=0,int yy=0){
x=xx;y=yy;
}
};
complex operator *(complex a,complex b){
return complex(((a.x*b.x%p+w*a.y%p*b.y%p)%p+p)%p,((a.x*b.y%p+a.y*b.x%p)%p+p)%p);
}
int read(){
int sum=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){sum=sum*10+ch-'0';ch=getchar();}
return sum*f;
}
complex ksm(complex x,int b){
complex tmp(1,0);
while(b){
if(b&1)tmp=tmp*x;
b>>=1;
x=x*x;
}
return tmp;
}
int ksm(int x,int b){
int tmp=1;
while(b){
if(b&1)tmp=tmp*x%p;
b>>=1;
x=x*x%p;
}
return tmp;
}
int work(int n){
if(p==2)return n;
if(ksm(n,(p-1)/2)+1==p)flag=true;
int a;
while(233){
a=random(0,p-1);
w=((a*a-n)%p+p)%p;
if(ksm(w,(p-1)/2)+1==p)break;
}
complex res(a,1);
complex ans(0,0);
ans=ksm(res,(p+1)/2);
return ans.x;
}
signed main(){
srand((unsigned)time(NULL));
p=read();n=read();
n%=p;
int ans1=work(n);
int ans2=p-ans1;
if(flag){printf("No Solution\n");return 0;}
if(ans1==ans2)printf("%lld\n",ans1);
else printf("%lld %lld",min(ans1,ans2),max(ans1,ans2));
return 0;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 本地部署DeepSeek后,没有好看的交互界面怎么行!
· 趁着过年的时候手搓了一个低代码框架
· 用 C# 插值字符串处理器写一个 sscanf
· 推荐一个DeepSeek 大模型的免费 API 项目!兼容OpenAI接口!