同余:(这里只讲整数的同余)
除以余数是,除以余数也是,那么就称与在模意义下同余,符号表示为。
然后,很容易得到一些性质:
- 自反性:
- 对称性:若,则
- 传递性:若,且,则。
这些都显然,不重要,重要的在后面。
同余意义下的加减乘:
为了简单阅读,加减乘就证明了,直接贴代码。
基于一个原理,先加减乘再取模和先取模再加减乘,结果是不影响的,需读者自己思考想清楚。
模p意义下的:(ps:这里代码的计算答案均赋值给c)
c=(a+b)%p;
模p意义下的:
c=(a%p-b%p+p)%p;
这里需要注意的是,%对负数的处理是,先对其绝对值取模,再将符号乘给它,所以我们要保证它结果为正数,需取模后加上模数再取模。
模p意义下的;
c=a*b%p;
不管是什么运算都要注意不要溢出,比如a和b均大于模数p,且相乘会溢出的话,应该改写乘c=(a%p)*(b%p)%p,这个需自行判断。
模意义下的除法:
思考一个问题,先除再模和先模再除,结果是否一样?
答案显然是否,因为会有除不尽的情况。
所以引入了逆元。
什么是逆元?
通俗来讲,的逆元就是。
那么我们要除以,相当于只要乘上即可。
当是模意义下的逆元,那么就有
逆元不是一定存在的!!
模意义下,的逆元存在的充要条件是。
举个例子,乘上在模意义下等于,不存在吧,这种情况就是没有逆元。
ps:准确来讲不应该是等于,而是与同余,当模数为时,任何数都与同余
那么现在考虑怎么求逆元:
先上费马小定理:
成立条件:为素数,且
然后就有了:
所以我们现在只要求就好了(前提要满足费马小定理),上模板:
//才疏学浅,不敢用位运算
ll fpow(ll a,ll n,ll mod)//快速幂
{
ll sum=1,base=a%mod;
while(n!=0)
{
if(n%2)sum=sum*base%mod;
base=base*base%mod;
n/=2;
}
return sum;
}
ll inv(ll a,ll mod)//逆元
{
return fpow(a,mod-2,mod);
}
那么现在要研究不满足费马小定理的逆元求法。
当不满足那么逆元不存在,所以我们只考虑不是素数。
要求方程的解。
等效成求的解,然后就有拓展欧几里得求解。
传送门
学完了加减乘除,就可以轻松实战一下了。
求解方程:
那么只要两边同时加上,再同乘即可。
就得到了:
是不是很快就上手了。
再介绍一下快速乘:
先来一个场景,模数,要算,那么取模后的和都不会大于,两个乘起来也不会爆long long,这时候开long long 就可以了。
那么如果模数,那么取模后相乘仍有爆long long的危险,这时候要怎么办,这里介绍两种办法。
一种是江湖邪术:__int128
顾名思义,就是128位存储的整数,大到。
不过它只能在Linux环境下能够使用,在大部分oj上都能用,本地编译器无法运行,可以用long long 过了样例再换掉提交。
它自带了各种基本运算,以及强制转换符号,但是没有特定的读入和输出,所以可以用long long读进来后,强制转化成(__int128)再进行运算。
或者手写一个读入和输出,原理就是用getchar()和putchar()以及char类型和__int128型的强制转换。
__int128读入读出模板:
ll read()
{
int X=0,w=0; char ch=0;
while(!isdigit(ch)) {w|=ch=='-';ch=getchar();}
while(isdigit(ch)) X=(X<<3)+(X<<1)+(ch^48),ch=getchar();
return w?-X:X;
}
void print(ll x)
{
if(x<0){putchar('-');x=-x;}
if(x>9) print(x/10);
putchar(x%10+'0');
}
同样可以适用于long long的读入,使用时只要把typedef long long ll;改成typedef __int128 ll;即可。
第二种办法就是快速乘:
原理和快速幂一样,只是乘法运算变成了加法运算,复杂度是log。
直接上o(log)的模板:
ll fmul(ll a,ll b,ll mod)
{
ll sum=0,base=(a%mod+mod)%mod;
while(b)
{
if(b%2)sum=(sum+base)%mod;
base=(base+base)%mod;
b/=2;
}
return sum;
}
还有一种o(1)的写法,因为C++内有128位的long double型,可以利用它优化成o(1)。
o(1)的快速乘模板:
ll fmul(ll x,ll y,ll mod)
{
ll tmp=(x*y-(ll)((long double)x/mod*y+1.0e-8)*mod);
return tmp<0?tmp+mod:tmp;
}
注意,o(1)快速乘因为原理是利用128位的long double,所以将__int128和其混用,并不能改善爆__int128的问题。
但是用o(log)的快速乘和__int128混用却可以解决模数大至__int128的乘法问题。
模意义下的开根号(二次剩余):
什么是剩余多出来的;遗留下来的:剩余物资。出自《现代汉语词典》
剩余就是某个物体分成若干份等量或不等量的物体,使用后还有没使用的,没使用的对于已使用过的就算是剩余。
总之就是余数。
剩余类: 设模为n,则根据余数可将所有的整数分为n类,把所有与整数a模n同余的整数构成的集合叫做模n的一个剩余类
剩余系: 对于特定的,一个整数集合里所有数对其取模后剩下来的集合。
完全剩余系: 从模n的每个剩余类中各取一个数,得到一个由n个数组成的集合,叫做模n的一个完全剩余系。差不多就是到。
什么是二次剩余
对于二次同余方程,若,且存在一个满足该方程,则称是模意义下的二次剩余,若无解,则称n为p的二次非剩余。
欧拉判别法:(书上还写了一个高斯引理,可以判定与p互素的整数是否是二次剩余,看不进去了)
对于是奇素数,我们有办法来判断是否为的二次剩余。
我们假设存在解使得。
式子转化后变成。
根据费马小定理,就有了,此时方程有解。
且,得到。
另外只需要证明等于时无解即可,此处证明略。
我们引入勒让德符号,其中不能被整除,整除时答案即为。
有个结论,对于为奇素数,若,则是的二次剩余,反之若,则是的非二次剩余。
还有三个定理:
- 若,则。
定理自行证明。
还有一个更流批 一点的定理,对于奇素数,方程,共有个使得方程有解,换句话说有一半的二次剩余。
现在问题来了!!!传送门
如何求解方程,其中为奇素数。
偶素数其实就一个嘛,特判一下就好了,就可以知道是素数的了。这显然可以用BSGS算法来求解。
我们考虑比快的做法,但是仅限于为奇素数的情况,如果没有这个条件,还是老老实实地BSGS吧。
考虑的做法:Cipolla算法
首先用欧拉判别法判断是否有解,的情况特殊考虑,没解就写个无解然后下一题。
然后有非解的话,则一定是两个解,互为相反数,即和。
我们先随机找一个,使得是的非二次剩余,因为非二次剩余占一半,所以期望大约两次就可以找到了。
于是乎,就是方程的解了!!
????????!
好吧我们来证明一下。
我们令,因为是二次非剩余,所以的值是不存在的,我们将数域扩张,引入类似复数的东西。
现在我们有了下面的东西,
- (这条好像没用到)
第一个根据定义。第二个是利用二次非剩余的欧拉判别性质。第三个是用二项式定理展开后,中间项都模消除了。
然后开始操作起来:
所以。
并且可以自证左边展开后是一个实数,否则若仍有虚部,则无法解决问题。
就证好了。
然后只要写一个复数类,再套快速幂就可以了。
上代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef struct cp
{
ll r,i;
cp(const int&x,const int &y){r=x,i=y;}
}cp;
cp mul(cp a,cp b,ll p,ll w)
{
return cp((a.r*b.r%p+a.i*b.i%p*w%p)%p,(a.r*b.i%p+a.i*b.r%p)%p);
}
cp cp_fpow(cp a,ll n,ll p,ll w)
{
cp sum(1,0),base=a;
while(n!=0)
{
if(n%2)sum=mul(sum,base,p,w);
base=mul(base,base,p,w);
n/=2;
}
return sum;
}
ll fpow(ll a,ll n,ll p)
{
ll sum=1,base=a%p;
while(n!=0)
{
if(n%2)sum=sum*base%p;
base=base*base%p;
n/=2;
}
return sum;
}
bool OulaPb(ll n,ll p)//欧拉判别
{
if(fpow(n,(p-1)/2,p)==p-1)return false;
else return true;
}
ll Cipolla(ll n,ll p)//无解返回-1
{
n=(n%p+p)%p;
if(n==0)return 0;
if(p==2&&n==1)return 1;
if(!OulaPb(n,p))return -1;
ll a=rand()%p*rand()%p;
while(OulaPb((a*a%p-n%p+p)%p,p))a=rand()%p*rand()%p;
cp ans=cp_fpow(cp(a,1),(p+1)/2,p,(a*a%p-n%p+p)%p);
return ans.r%p;
}
int main()
{
srand(time(0));
ll T;
scanf("%lld",&T);
while(T--)
{
ll n,p;
scanf("%lld%lld",&n,&p);
ll ans=Cipolla(n,p);
if(ans==-1)printf("Hola!\n");
else if(ans==(p-ans)%p)printf("%lld\n",ans);
else printf("%lld %lld\n",min(ans,p-ans),max(ans,p-ans));
}
return 0;
}
关于不为素数的,书上有介绍一种方法,将模数因子分解,成若干个奇素数,求出若干个方程的解,然后用中国剩余定理合并。
比如。
其中。
根据对成立。
可以得到,和。
每个方程分别有两个解,然后用中国剩余定理合并,得到四个解。
具体操作就不说了。
最后再扔一个数论四大定理:
- 威尔逊定理:p可整除(p-1)!+1是p为质数的充要条件。
- 欧拉定理:若,则,其中为欧拉函数。
- 孙子定理:用来求解模数两两互质的同余方程组,传送门。
- 费马小定理:若为素数,且,则。(其实为素数时,)。
来源:CSDN
作者:Zimba_
链接:https://blog.csdn.net/weixin_43785386/article/details/104086765