数论(1) | 拓展欧几里得

1 引例

在开始枯燥乏味的数论推理之前,我们先来看两只有趣的小青蛙吧

(洛谷P1516)两只青蛙在网上相识了,它们聊得很开心,于是觉得很有必要见一面。它们很高兴地发现它们住在同一条纬度线上,于是它们约定各自朝西跳,直到碰面为止。

我们把这两只青蛙分别叫做青蛙A和青蛙B,并且规定纬度线上东经0度处为原点,由东往西为正方向,单位长度1米,这样我们就得到了一条首尾相接的数轴。设青蛙A的出发点坐标是x,青蛙B的出发点坐标是y。青蛙A一次能跳m米,青蛙B一次能跳n米,两只青蛙跳一次所花费的时间相同。纬度线总长L米。现在要你求出它们跳了几次以后才会碰面。

这道题一上来很容易想到while,每跳一次执行一次加x(y)模p,相等时输出循环次数即可。

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 typedef long long LL;
 4 int main(){
 5     LL m,n,x,y,L,p=0;
 6     cin>>m>>n>>x>>y>>L;
 7     while (m!=n){
 8         m=(m+x)%L;
 9         n=(n+y)%L;
10         p++; 
11     }
12     cout<<p;
13     return 0;
14 } 

看一眼数据范围:

其中,0<x≠y < =2000000000,0 < m、n < =2000000000,0 < L < =2100000000。

好的,刚刚除了头文件都白敲了……

2 拓展欧几里得

其实倒也不至于完全白敲啦,要不然我把这个程序放在这儿是搞笑嘛?

我们整理一下,应该很容易可以得到一个公式:

(m+px)%L=(n+py)%L

也可以写成:

((m-n)+(x-y)p)%L=0

我们不妨设kL=(m-n)+(x-y)p,显然要求k,p∈Z

移项,得:

kL+p(y-x)=m-n

令 a=L,b=y-x,c=m-n

则:

ak+bp=c(k,p∈Z)

这样的式子我们把它叫做不定方程,而拓展欧几里得算法可以求出不定方程的一组特解

这儿还要再补充一个规律

当c是gcd(a,b)的倍数时,(k,p)有无穷多解

当c不是是gcd(a,b),(k,p)无整数解

这个证明挺复杂的,建议自己写几个例子意会一下就可以了。

于是现在问题转换成了这样的两个小问题:

(1)怎么用拓展欧几里得求出这个特解?
(2)怎么推出一个最小正整数解kmin

3 先求一个特解

我们都学过辗转相除法:
gcd(a,b)=gcd(b,a%b)

不断递推直到 b=0 ,此时a即为最大公约数
现在我们来看两个式子:

(1)   a*k1+b*p1=gcd(a,b)
(2)   b*k2+(a%b)*p2=gcd(b,a%b)

我们知道 gcd(a,b) = gcd(b,a%b) ,如果我们只要找出k1和k2的关系、p1和p2的关系,我们就能一层一层递推下去,直到 a%b = 0,对吧?

因为 gcd(a,b) = gcd(b,a%b) ,两式联立,得:
a*k1+b*p1 = b*k2+(a%b)*p2
其中a%b可以转换为 a-(a/b)*b,式子就变为:
a*k1+b*p1 = b*k2+(a-(a/b)*b)*p2

移项一下:

a*k1+b*p1 = a*k2+b*(k2-(a/b)*p2)

现在我们要确保这个式子成立,则 (k1,p1)~(k2,p2) 应该要满足这样的条件

条件1:k1 = p2(等式两边a的系数相同)
条件2:p1 = k2-(a/b)*p2(等式两边b的系数相同)

(这两个条件应该是等式的充分不必要条件,但这就够了,我们只要保证式子可以递推下去。)

 

当我们一层一层递推下去,

根据辗转相除法的规律,我们可以知道式子(2)的 a%b 早晚会变成 0。

此时可以得到方程的一个解 k=1,p=0

在根据上面的条件1和条件2一层一层递归回去。

就可以求得 ak+bp=gcd(a,b) 的特解啦。

当 c 是 gcd(a,b) 的倍数时,方程两边同乘 c/gcd(a,b) 即可。

当 c 不是 gcd(a,b) 的倍数时,方程无解。

4 推出最小正整数解kmin

很简单,直接上一个结论:

对于 a*k+b*p=c

让 k 增加 b/c ,让 p 减少 a/c ,等式两边还相等。

推理过程很简单,手算一下就出来了。

那么现在我们要做的是变换 k 的大小,找到 kmin

 

(1)当 k 为正数时,k≥kmin  ,   此时,k 要不停的减去 b/c , 即:kmin=k%(b/c);

(2)当 k 为负数时,k<kmin  , 此时,k 要不停的加上 b/c , 即:kmin=(k%(b/c)+(b/c)).

(这个式子看起来复杂,其实随便写个例子就理解了。比如k=-8,b/c=3,k%(b/c)就是-2,在加一次3,就得到最终的答案1了)

综上:kmin=(k%(b/c)+(b/c))%(b/c).

5 代码实现

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 typedef long long LL;
 4 LL exgcd(LL a,LL b,LL &x,LL &y){//对于不完全为 0 的非负整数 a,b,必然存在整数对 x,y ,使得 gcd(a,b)=ax+by
 5     if (b==0){
 6         x=1;y=0;
 7         return a;
 8     } 
 9     LL r=exgcd(b,a%b,x,y);
10     LL t=x;
11     x=y;
12     y=t-(a/b)*y;
13     return r;
14 }
15 int main(){
16     LL x0,y0,m,n,L;
17     cin>>x0>>y0>>m>>n>>L;
18     LL a=L,b=m-n,c=y0-x0;//调成ax+by=c模式 
19     if (b<0){
20         b=-b;c=-c;
21     }//保证b>0 
22     LL x,y;
23     LL s=exgcd(a,b,x,y);
24     if (c%s==0){
25         y=y*(c/s);//先放大再求余,否则无法找到离0最近的点 
26         y=( y%(a/s)+(a/s) )%(a/s);//把很小的负数变成(大于-a/s)的负数 
27         cout<<y<<endl; 
28     }//求出y的最小正整数解 
29     else cout<<"Impossible";//无解的情况 
30     return 0;
31 } 
posted @ 2021-06-19 23:33  奇思妙想张霁羊  阅读(105)  评论(0)    收藏  举报