1.欧拉定理及其推论与特殊情况(费马小定理)
欧拉定理:对于任意两个互质的数a,n,有,可以通过简化剩余系的方法证明。
欧拉定理的推论:对于任意两个互质的数a,n,有(其中b为任意整数)。特别地,当a,n是任意的两个数,且
时,有
。
费马小定理:若p为质数,有。
有了这3个公式,就可以解决一些幂形式的同余方程了。
例题:AcWing 202.最幸运的数字(求解指数型同余方程)
类型1:求解特殊的指数型方程(可用欧拉定理)
本题较为困难,要求我们对于欧拉定理有足够深刻的理解并且要有极强的数论处理技巧。首先很容易想到把888……88这样形式的数转化为:,那么关键怎么求出x的值呢?
引理:
1.整除的重要性质1:若,
,则有
。
2.整除的重要性质2:若a,c两数互质,且,则有
。
那么接下来问题就不再困难:利用这两个性质可以将式子最终化成:
那么这类未知数在指数上的同余方程要怎么解呢?当然要联系欧拉定理啦!我们猜想x是的因数。这很容易证明出来。所以我们只要枚举
的因数并一个一个验证就可以了。
代码如下:
#include<iostream>
using namespace std;
typedef long long ll;
ll gcd(ll a,ll b){
if(!b) return a;
return gcd(b,a%b);
}
ll get(ll x){
ll res=x;
for(ll i=2;i*i<=x;i++){
if(x%i==0){
res=res/i*(i-1);
while(x%i==0) x/=i;
}
}
if(x>1) res=res/x*(x-1);
return res;
}
ll mul(ll a,ll b,ll p){
ll ans=0;
while(b){
if(b&1) ans=(ans+a)%p;
a=(a+a)%p;
b>>=1;
}
return ans;
}
ll qmi(ll a,ll b,ll p){
ll ans=1;
while(b){
if(b&1) ans=mul(ans,a,p);
a=mul(a,a,p);
b>>=1;
}
return ans;
}
int main(){
ll L;
int cnt=0;
while(cin>>L && L){
ll d=gcd(L,8);
L=9*L/d;
if(gcd(L,10)!=1){//欧拉定理的条件
printf("Case %d: 0\n",++cnt);
continue;
}
ll eul=get(L);
ll res=1e18;
for(ll i=1;i*i<=eul;i++){
if(eul%i==0){
if(qmi(10,i,L)==1) res=min(res,i);
if(qmi(10,eul/i,L)==1) res=min(res,eul/i);
}
}
if(res==1e18) res=0;
printf("Case %d: %lld\n",++cnt,res);
}
return 0;
}
类型2:一般形式的指数型方程
(其中a,p互质)
这类题目要采用特殊的算法:Baby Step,Giant Step algorithm(小步大步算法)。大致思路是:设x=i * t - j。(其中t=,且
)那么容易知道有
,即
。那么我们考虑预处理出所有的
%p的值,存储在一个Hash表中。然后同样的把等号右边的式子的所有可能结果算出来,然后在Hash表中寻找对应相等的值,如果答案符合题意就输出。
但是为什么要取一个这样的t呢?这需要证明:
代码如下:
ll baby_step(ll a,ll b,ll p){
map<ll,ll> has;//Hash表
ll t=(ll)sqrt(p)+1;
for(ll j=0;j<t;j++){
ll val=b%p*qmi(a,j,p)%p;
has[val]=j;
}
a=qmi(a,t,p);
if(!a) return b==0 ? 1 : -1;//a=b=0的情况
for(ll i=0;i<=t;i++){
ll val=qmi(a,i,p);
ll j=has.find(val)==has.end() ? -1 : has[val];
if(j>=0 && i*t-j>=0){
return i*t-j;
}
}
return -1;
}
例题: TJOI2007、
2.Exgcd算法(求解线性同余方程)
引理:Bezout定理
对于任意整数a,b,存在整数x,y,使得ax+by=gcd(a,b)。
扩展到n元形式:对于任意整数,存在整数
,使得
。此处证明可以考虑用合并递推的思想:首先令A=gcd(a,b),B=c,那么显然存在解x,y使得Ax+By=gcd(A,B)成立,那么带入式子计算可知三元的形式仍然成立,以此类推,不断合并即可证明。
这个就很简单了,可以自己复习啦。但是复习的时候印象不深是怎么回事?原来是采用了自己不好理解的写法,这里有一种非常直观容易理解的方法,忘了也可以推的办法:
#include<iostream>
using namespace std;
int exgcd(int a,int b,int &x,int &y){
if(!b){
x=1;y=0;
return a;
}
int d=exgcd(b,a%b,x,y);
int temp=x;//要注意这里所有的变量所存的值的含义
x=y;
y=temp-a/b*x;
return d;
}
int main(){
int a,b,x,y;
cin>>a>>b;
exgcd(a,b,x,y);
cout<<(x%b+b)%b;
return 0;
}
推荐模板题:
AcWing 203.同余方程
提示:这类问题可以用于解一元二次的不定方程(或是一元一次的同余方程)。
例题1:AcWing 222.青蛙的约会
这题实际上很简单,首先可以列出两个同余方程(均在模L意义下):
那么二式相减就可以得到一个一元一次同余方程,可以使用exgcd解决。
3.中国剩余定理及其扩展(CRT and ExCRT)
首先介绍CRT:见《算法进阶指南》P154。CRT的条件很严苛,要求模数必须两两互质。
注意这里求出来的解是一个特解,其通解的形式要特别注意:相当于把解对于所有模数的乘积取模了。
模板题:AcWing 223.阿九大战朱最学
#include<iostream>
using namespace std;
const int N=11;
typedef long long ll;
ll a[N],b[N];
ll exgcd(ll a,ll b,ll &x,ll &y){
if(!b){
x=1,y=0;
return a;
}
ll d=exgcd(b,a%b,x,y);
ll temp=x;
x=y;
y=temp-a/b*x;
return d;
}
int main(){
int n;
cin>>n;
ll m=1;
for(int i=1;i<=n;i++){
cin>>a[i]>>b[i];
m*=a[i];
}
ll ans=0;
for(int i=1;i<=n;i++){
ll M=m/a[i];
ll x,y;
ll d=exgcd(M,a[i],x,y);
ans=(ans+b[i]*M%m*x%m)%m;
}
cout<<(ans+m)%m;
return 0;
}
但是ExCRT就没有这个约束条件了,随便什么都行。那么这时考虑使用数学归纳法,假设已经求出了前k-1个方程的一个特解x,那么通解可以表示为x+tm(),其中m是前面k-1个模数的lcm。那么求前k个方程的特解相当于求一个整数t,使得x+tm
ak(mod mk),这是一个线性同余方程,可以用扩欧解决。最后前k个方程的特解就可以表示为x'=x+tm了(
)。
代码如下:
#include<iostream>
using namespace std;
typedef long long ll;
ll exgcd(ll a,ll b,ll &x,ll &y){
if(!b){
x=1,y=0;
return a;
}
ll d=exgcd(b,a%b,y,x);
y-=a/b*x;
return d;
}
ll mod(ll a,ll b){
return (a%b+b)%b;
}
int main(){
int n;
cin>>n;
ll a1,a2,m1,m2,k1,k2,ans;
cin>>a1>>m1;//这里采用迭代的方法来求解
for(int i=2;i<=n;i++){
cin>>a2>>m2;
ll d=exgcd(a1,-a2,k1,k2);
if((m2-m1)%d!=0){//判断方程是否有解
cout<<-1<<endl;
return 0;
}
k1=mod(k1*(m2-m1)/d,abs(a2/d));
m1=a1*k1+m1;
a1=abs(a1/d*a2);
}
cout<<m1<<endl;
return 0;
}
总结:同余是数论中非常重要的板块,常会用到同余与整除的一些重要性质,所以我们要多翻看数论书,多复习这些结论,并结合一些练习,使得我们可以做到灵活运用。