写在前面:
这篇博客是我在[◹]对 算术基本定理 的研究 中的一部分
by celebi-yoshi
目录 |
拓展欧几里得算法能解决的问题有:
① 已知正整数a,b,求一组p,q使满足p*a+q*b ≡ 0 (MOD GCD(a,b))
② 已知正整数a,b,素数p,求一个整数x使满足x*a ≡ b (MOD p)
③ 已知正整数a,b,素数p,求一个整数x使满足ax ≡ b (MOD p)
④ 已知正整数a,b,c,求一个整数x使满足x*a ≡ b (MOD c)
⑤ 已知正整数a,b,c,求一个整数x使满足ax ≡ b (MOD c)
⑥ 解线性方程组
-
已知正整数a,b,求一组p,q使满足p*a+q*b ≡ 0 (MOD GCD(a,b))
现在已经有了a-b == GCD(a,b) * (n1-n2),那就可以再来个直接点的
GCD(a,b) + (p*n1+q*n2) == GCD(a,b)
即p*a+q*b == GCD(a,b)
想要求出一组p,q∈Z,满足上式
首先套用欧几里得算法的逻辑:
结合算术基本定理,有
GCD(a,b) == P1min(a1,b1)P2min(a2,b2)......Pnmin(an,bn)
a MOD b == P1a1-b1P2a2-b2......Pnan-bn(执行的条件为ai>=bi)
则能得到GCD(a,b) == GCD(b,a MOD b)
那么就有p*a+q*b == GCD(a,b) == GCD(b,a MOD b)
这样就可以写一个递归函数,一层一层MOD下去
这样化简,直到ai MOD bi == 0
即p*GCD(a,b) == GCD(a,b),那么在最后一层里面p==1
所以在拓展欧几里得里面,bi==0这一层中,ai即为GCD(a,b)
(到此为止和欧几里得算法一模一样,我甚至是复制的欧几里得算法)
而如果想要求出p和q,那么有一个回溯的过程就好了
如何回溯得到p和q呢?
来找一下相邻的层数间pi和pi+1,qi和qi+1的关系
因为有p*a+q*b == GCD(a,b)
则在每一层中:
GCD(a,b)
== pi*a+qi*b
== pi+1*b + qi+1*(a MOD b)
== pi+1*b + qi+1*(a - (a/b)*b)
== pi+1*b + qi+1*a - qi+1*(a/b)*b
== qi+1*a + pi+1*b - qi+1*(a/b)*b
== qi+1*a + (pi+1 - qi+1*(a/b))*b
即每层中pi == qi+1,qi == pi+1 - qi+1*(a/b)
然后就可以回溯了!
然后就能写出不返回gcd,单纯求出一组p,q的拓展欧几里得了!
关于应用①中的多解问题,见[◹]拓展欧几里得算法的多解
C++:
1 #include<bits/stdc++.h> 2 #define OUT(x) cout<<#x<<":"<<x<<endl; 3 4 using namespace std; 5 6 int x,y; 7 8 void exgcd(int a,int b) 9 { 10 if(!b){x=1;y=0;} 11 else{ 12 exgcd(b,a%b); 13 int tmp=x; 14 x=y;y=tmp-(a/b)*y; 15 } 16 } 17 18 int main(int argc,char *argv[],char *enc[]) 19 { 20 exgcd(12,17); 21 OUT(x);OUT(y); 22 return 0; 23 }
Java:
1 class Pony{ 2 3 static int x,y; 4 5 static void exgcd(int a,int b) 6 { 7 if(b==0){x=1;y=0;} 8 else{ 9 exgcd(b,a%b); 10 int tmp=x; 11 x=y;y=tmp-(a/b)*y; 12 } 13 } 14 15 public static void main(String[] args) throws Exception 16 { 17 int a,b;a=54;b=27; 18 exgcd(a,b); 19 System.out.println(x+" "+y); 20 } 21 }
上面这个版本是我写的,比较诡异,来个常见的版本
C++:
1 #include<bits/stdc++.h> 2 #define OUT(x) cout<<#x<<":"<<x<<endl; 3 4 using namespace std; 5 6 int x,y; 7 8 int exgcd(int a,int b) 9 { 10 if(!b){ 11 x=1;y=0; 12 return a; 13 } 14 int ret=exgcd(b,a%b); 15 int tmp=x; 16 x=y;y=tmp-(a/b)*y; 17 return ret; 18 } 19 20 int main(int argc,char *argv[],char *enc[]) 21 { 22 OUT(exgcd(12,17)); 23 OUT(x);OUT(y); 24 return 0; 25 }
Java:
1 class Pony{ 2 3 static int x,y; 4 5 static int exgcd(int a,int b) 6 { 7 if(b==0){ 8 x=1;y=0; 9 return a; 10 } 11 else{ 12 int ret=exgcd(b,a%b); 13 int tmp=x; 14 x=y;y=tmp-(a/b)*y; 15 return ret; 16 } 17 } 18 19 public static void main(String[] args) throws Exception 20 { 21 int a,b;a=54;b=27; 22 exgcd(a,b); 23 System.out.println(x+" "+y); 24 } 25 }
仔细感受一下其中的差别啦~
首先判断一下,如果a%p==0,那么无解
如果a%p!=0,那么必定有GCD(a,p)==1
利用分治的方法,先利用上述①求得一个正整数x‘,使满足
x'*a + tmp*p ≡ gcd(a,p) (MOD p)
即x'*a ≡ 1 (MOD p)
然后易得
x=x'*b
x*a ≡ b (MOD p)
C++:
1 #include<bits/stdc++.h> 2 3 using namespace std; 4 5 int a,b,p,x,y; 6 7 int exgcd(int a,int b) 8 { 9 if(!b){ 10 x=1;y=0; 11 return a; 12 } 13 int ret=exgcd(b,a%b); 14 int tmp=x; 15 x=y;y=tmp-(a/b)*y; 16 return ret; 17 } 18 19 int main(int argc,char *argv[],char *enc[]) 20 { 21 scanf("%d%d%d",&a,&b,&p); 22 if(exgcd(a,p)!=p){ 23 x*=b; 24 printf("%d*%d ≡%d (mod %d) ",x,a,b,p); 25 } 26 else printf("NOPE "); 27 28 return 0; 29 }
Java:
1 import java.util.Scanner; 2 3 class Pony{ 4 5 static int a,b,p,x,y; 6 7 static int exgcd(int a,int b) 8 { 9 if(b==0){ 10 x=1;y=0; 11 return a; 12 } 13 else{ 14 int ret=exgcd(b,a%b); 15 int tmp=x; 16 x=y;y=tmp-(a/b)*y; 17 return ret; 18 } 19 } 20 21 public static void main(String[] args) throws Exception 22 { 23 Scanner cin=new Scanner(System.in); 24 25 a=cin.nextInt(); 26 b=cin.nextInt(); 27 p=cin.nextInt(); 28 if(exgcd(a,p)!=p){ 29 x*=b; 30 System.out.printf("%d*%d ≡ %d (mod %d) ",x,a,b,p); 31 } 32 else System.out.println("NOPE"); 33 } 34 }
②中的无解是因为a%p==0,a为素数p的倍数
如果a不为p的倍数,gcd(a,p)==1
分治,结合①可以解得x'*a ≡ 1 (mod p)
在④中其实同理,只是有的时候gcd(a,c)不一定是1
我们想要gcd(a,c)为1
在同余中有一个神奇的性质: A' == a'*d' B' == b'*d' C' == c'*d' 若A' ≡ B' (mod C') 则a'*d' ≡ b'*d' (mod c'*d') 即a' ≡ b' (mod c') 其中,a',b',c',d',A',B',C'都为整数 暂且叫这条性质为放缩 |
求解x*a ≡ b (MOD c),想要延续②中的思路
如果gcd(a,c)不是b的因子或b不等于0,那么可以知道gcd(a,c)!=1,而方程一定无解(下面有证明)
而如果gcd(a,c)为b的因子或b等于0,那么有
a==a'*gcd(a,c)
c==c'*gcd(a,c)
b==b'*gcd(a,c)
有x*a ≡ b (MOD c)
则x*a'*gcd(a,c) ≡ b'*gcd(a,c) (MOD c'*gcd(a,c))
根据放缩,即x*a' ≡ b' (MOD c')
此时,gcd(a',c')==1
这样一来,就完美转化为了②中的思路,可以求解了
但是,如果b中没有gcd(a,c)这个因子或b不等于0,是否就没有解呢?
答案是,没有解,一定没有解
同余方程x*a ≡ b (mod c)有解x的充分必要条件是b%gcd(a,c)==0
证明如下:
设整数A,C,a,b,c,d,其中A==a*d,c==c*d,abcd两两互质
我们想要通过缩放来得到gcd(a,c)==1,这是我们的目标
设满足A ≡ b (mod C)
则a*d ≡ b (mod c*d)
根据放缩,有a ≡ b/d (mod c)
∵abcd两两互质
∴d不是b的因数且b不等于0
∴b/d不是整数
而在同余方程中的每个变元都必须为整数
所以出现分数的情况视为违规
则这个同余式不成立
所以同余方程x*a ≡ b (mod c)有解x的充分必要条件是b%gcd(a,c)==0
由以上证明可以看出来在同余方程中,出现无解情况的本质
代码如下:
C++:
1 #include<bits/stdc++.h> 2 3 using namespace std; 4 5 int a,b,c,x,y; 6 int times; 7 8 int exgcd(int a,int b) 9 { 10 if(!b){ 11 x=1;y=0; 12 return a; 13 } 14 int ret=exgcd(b,a%b); 15 int tmp=x; 16 x=y;y=tmp-(a/b)*y; 17 return ret; 18 } 19 20 int main(int argc,char *argv[],char *enc[]) 21 { 22 scanf("%d%d%d",&a,&b,&c); 23 24 int tmp=exgcd(a,c); 25 26 if(b%tmp!=0) printf("NOPE "); 27 else{ 28 while(b%tmp==0){ 29 a/=tmp; 30 b/=tmp; 31 c/=tmp; 32 ++times; 33 } 34 35 if(tmp!=c){ 36 x*=b; 37 printf("%d*%d ≡ %d (mod %d) ",x,a*times*tmp,b*times*tmp,c*times*tmp); 38 } 39 else printf("NOPE "); 40 } 41 42 return 0; 43 }
Java:
1 import java.util.Scanner; 2 3 class Pony{ 4 5 static int a,b,c,x,y; 6 static int times; 7 8 static int exgcd(int a,int b) 9 { 10 if(b==0){ 11 x=1;y=0; 12 return a; 13 } 14 else{ 15 int ret=exgcd(b,a%b); 16 int tmp=x; 17 x=y;y=tmp-(a/b)*y; 18 return ret; 19 } 20 } 21 22 public static void main(String[] args) throws Exception 23 { 24 Scanner cin=new Scanner(System.in); 25 26 a=cin.nextInt(); 27 b=cin.nextInt(); 28 c=cin.nextInt(); 29 30 int tmp=exgcd(a,c); 31 32 if(b%tmp!=0) System.out.println("NOPE"); 33 else{ 34 while(b%tmp==0){ 35 a/=tmp; 36 b/=tmp; 37 c/=tmp; 38 ++times; 39 } 40 41 if(tmp!=c){ 42 x*=b; 43 System.out.printf("%d*%d ≡ %d (mod %d) ",x,a*times*tmp,b*times*tmp,c*times*tmp); 44 } 45 else System.out.println("NOPE"); 46 } 47 } 48 }
详见[◹]拓展Baby-step giant-step算法
俗称的拓展中国剩余定理(虽然这样叫很不准确)