exgcd(扩展欧几里得),欧几里得
目录
欧几里得定理及证明及代码
扩展欧几里得及证明及代码
裴属定理及证明
扩欧x,y的解的转换及求取最小整数解的证明
T1青蛙的约会
负数的模运算,代码修改过程
T2倒酒
T3荒岛野人Savage
(我写博客基本都是对自己不太熟练的地方做一个梳理,所以大概不太会考虑读者的阅读体验)
欧几里得:
用来求最大公约数的一个公式
公式:gcd(a,b)=gcd(b,a%b)=d;
证明:设 a=md,b=nd,显然,m与n互质(它俩要不互质那最大公约数就不是d,我们已经规定最大公约数为d)
再设 a=qb+r md=qb+r
那么 r=a-qb=md-ndq=d(m-qn)
gcd(b,r)=gcd(nd,d(m-qn))
因为m与n互质,所以n与m-qn互质
m和n本身就是互质的,你无论在m里怎么减去一个n的倍数,该互质还是互质,如果减去一个n的倍数就不互质了,即m-qn=kn,那m=n(q+k),说明原本就不互质
所以gcd(b,r)=d;r=a%b 得证
代码
查看代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
typedef long long ll;
const int mx=30;
int gcd(int a,int b){
if(b==0)return a;
return gcd(b,a%b);
}
void Solve(){
int a,b;
scanf("%d%d",&a,&b);
int d=gcd(a,b);
printf("%d\n",d);
}
int main(){
Solve();
return 0;
}
扩展欧几里得:
在求a,b的最大公约数的同时顺带求了 ax+by=gcd(a,b)的以一组解
证明:这个其实也简单
ax1+by1=gcd(a,b);
bx2+a%by2=gcd(b,a%b);
因为gcd(a,b)=gcd(b,a%b)
所以 ax1+by1=bx2+a%by2
因为 a%b=a-a/b*b (显然a/b是指int下的向下取整的整数解)
所以 ax1+by1=bx2+(a-a/b*b)y2=bx2+ay2-a/b*by2=ay2+b(x2-a/b*2)
ax1+by1=ay2+b(x2-a/b*y2)
那么我们就可以令 x1=y2,y1=x2-a/b*y2;得出答案
代码
查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
typedef long long ll;
const int mx=30;
int exgcd(int a,int b,int &x,int &y){
if(b==0){
x=1;
y=0;
return a;
//先求出 a*x+y*0=gcd(a,b)=a 的一组解
}
int ret=exgcd(b,a%b,x,y);
int t=x;
x=y;
y=t-a/b*y;
//再根据公式回溯时把解代换出来
return ret;
}
void Solve(){
int a,b;
scanf("%d%d",&a,&b);
int x,y;
int d=exgcd(a,b,x,y);
printf("d=%d x=%d y=%d\n",d,x,y);
}
int main(){
Solve();
return 0;
}
做题心得及体会:
俗话说的好(我说的):知识掌握的再透有什么用?你得会做题啊
其实如果真去做题就会发现有很多技巧和推论真不是好搞的
如果是实际题目 基本最后都可以归纳成求解ax+by=m
说到这里让我们介绍一下裴蜀定理
-
裴蜀(贝祖)定理
定理:若ax+by=m有解,那么m一定是gcd(a,b)的若干倍
证明:我的证明可能略显奇怪(以后没有特殊说明默认d=gcd(a,b))
设ax+by=d ,
c*ax+c*by=d*c 显然
d*c=m 得证
这好像根本就没有证明啊
不过这确实是做题最常用的推论
换个思路
直接设ax+by=m
d一定可以被a,b整除
d一定可以被ax,by整除
d一定可以被ax+by整除
ax+by就是m 得证
大概就是这样,我们求出的ax+by=gcd(a,b)就可以扩展到任意解了
顺带把那俩也证一下
x,y的解的转换及求取最小整数解:
这个真的做题必用,看不懂证明也要把代码背过
再设ax1+by1=d
ax+by-ax1-by1=0
a(x-x1)+b(y-y1)=0
两边同除d
a(x-x1)/d=-b(y-y1)/d
因为gcd(a/d,b/d)=1; d本身就是它们的gcd,除去d它们的公因数就只剩1了
所以 (x-x1)是 b/d的整倍数
这个因为其实并不怎么所以。详细说一下:首先,a/d*(x-x1)是b/d的整倍数
设 a,b互质 k=ma=nb 就是说,k除去b后,依旧是a的整倍数,k除去的因数中没有一个与a相关 勉强算解释了一下
继续 : x-x1=b/d*t x=x1+b/d*t 由此可以由一组解推出无穷多的解
y这里同理 只不过注意一正一负
y=y1-b/d*t 这是显然,上面原式本身就有一个负,总得给一个,且加和不变,一个大了另一个总得小
这里强调第一次,在做所有的扩欧题,无论是使用推论还是怎样,永远不要忘了你求解的是原式,原式
那么这个同样可以看出 所有x之间有一个加b/d*t的线性关系
x(任意且为正)=xmin+b/d*t 把所有的b/d*t消去就是xmin
所以要求最小正整数(最小负没有)解:xmin=x%(b/d) 是不是很妙
下面说的这几道题洛谷上都有
青蛙的约会
这个题显然,就是求 (x+km)%L=(y+kn)%L
求出这个k,把上面的式子到换一下 先换成同余式,再根据同余恒等变换
(x+km) = (y+kn) (mod L)
(x+km)/L=t......(y+kn) (这里的(x+kn)和(y+kn) 不一定互质)
(x+km)=L*t.....(y+kn)
(x+km)-(y+kn)=L*t
其实在用exgcd去求逆元的时候也是这么一套流程,甚至求逆元比这个还简单一些
在做扩欧的时候,关键是找出ax+by=m的形式
所以转换上式 k(m-n)-l*t=y-x 设a=m-n,b=t
ka-bt=y-x
那么这里注意,问题来了,且问题很复杂
我们可以直接将其转化为 ka+bt=y-x
只需要最后记得将t取负即可
但是,如果 a<0呢
这里提一下我犯过的nt错误: ax-by=-ax+by 把b的负号给到a上,这是错的错的!
再次强调,我们要求解的原式永远是: ka-bt=y-x
根据我的实践,-7和3 3和-7 4和-16 -16和4 的d 在c++下d的绝对值是正数的d,但是d往往一正一负我也不知道为啥 且-7和3是-1,-16和4是4
但是,但是,它们求解出的x,y都是对的
总之就是 你去求解原式,不管d有多么离谱,答案就是那个答案,一定不会错,但是本题要求x1肯定得是最小正整数
所以问题来了,a可能为负,y-x也可能为负,怎么搞使得我的答案是最小正整数
首先引理,c*x1=c*x+b/d 它们之间也存在这个关系,这是很显然的
所以可以先直接求解出来(不进行任何换号操作,当然默认换y的号,y如果要求输出记得换号)
再乘上(y-x)/d ,再写个while把x的值正过来即可
这里补一句,自己回看差点没看懂
求解出来的 x1是 ax1+by2=gcd(a,b)的一个解
但实际上他应该等于 y-x ,所以这里我们要把x1乘上一个(y-x)/d
代码
查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
typedef long long ll;
const int mx=1000;
ll x,y,m,n,l;
ll exgcd(ll a,ll b,ll &x1,ll &y1){
if(b==0){
x1=1;
y1=0;
return a;
}
ll ret=exgcd(b,a%b,x1,y1);
ll t=x1;
x1=y1;
y1=t-a/b*y1;
return ret;
}
void Solve(){
scanf("%lld%lld%lld%lld%lld",&x,&y,&m,&n,&l);
ll a=y-x,b=m-n;
ll x1,y1;
ll an=exgcd(b,l,x1,y1);
if(a%an!=0){
printf("Impossible\n");
return ;
}
x1=x1*(a/an);
x1=x1%(l/an);
while(x1<=0){
if(l/an<0)x1=x1-l/an;
else x1=x1+l/an;
}
printf("%lld\n",x1);
}
int main(){
Solve();
return 0;
}
然后还有两个问题,明天再说,干饭去了
其实也就是一个了,首先上面那个while可以转换成一行
x1=((x1*(a/an))%(l/an)+(l/an))%(l/an);
不过这要保证an为正
为什么可以这么转换,就是关于mod运算在负数方面的操作了,再把这个问题解决,基本上放眼一看,扩欧全是一种套路模型(如果没有跟别的东西结合)(仅代表个人观点)
负数的mod运算
负数mod在不同语言下的答案往往很不一样
这里仅仅介绍在c++下的运算(因为我用c++ ----大雾),(c++跟系统计算器的有一小些地方还不一样,当然这跟不同计算器的不同版本甚至类型也有关,所以做题时关于负数mod一定要写代码,不要直接相信计算器)
说取模之前得先说除法
除法:
向零取整(趋零截尾)(truncate toward zero):向0方向取最接近精确值的整数,换言之就是舍去小数部分,因此又称截断取整。在这种取整方式下
7/4=1 7/(-4)=-1 6/3=2 6/(-3)=-2 -7/-4=1 (这里都是int类型)
c++都是采用truncate除法,这样可以使得,(-a)/b==-(a/b) 一定成立
mod法:(魔法??临时想的名字感觉很帅)
设a=qb+r
一般都会满足 r=a-(a/b)*b
对C++而言,该式非常重要,这是解决负数取模问题的关键
7%(-4)
7/(-4)=-1 余数=7-(-4)*(-1)=3 7%(-4)=3
(-7)%4
(-7)/4=-1 余数=(-7)-4*(-1)=-3 (-7)%4=-3
(-7)%(-4)
(-7)/(-4)=1 余数=(-7)-(-4)*1=-3 (-7)%(-4)=-3
所以我们可以显然得出 即使在负数mod中,余数的绝对值要小于模数的绝对值
所以如果保证了an为正
直接x1=((x1*(a/an))%(l/an)+(l/an))%(l/an);即可
代码
查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
typedef long long ll;
const int mx=1000;
ll x,y,m,n,l;
ll exgcd(ll a,ll b,ll &x1,ll &y1){
if(b==0){
x1=1;
y1=0;
return a;
}
ll ret=exgcd(b,a%b,x1,y1);
ll t=x1;
x1=y1;
y1=t-a/b*y1;
return ret;
}
void Solve(){
scanf("%lld%lld%lld%lld%lld",&x,&y,&m,&n,&l);
ll a=y-x,b=m-n;
if(b<0){
b=-b;
a=-a; //a跟b配套,b换a也换
//以此来保证模数为正
}
ll x1,y1;
ll an=exgcd(b,l,x1,y1);
if(a%an!=0){
printf("Impossible\n");
return ;
}
x1=((x1*(a/an))%(l/an)+(l/an))%(l/an);
printf("%lld\n",x1);
}
int main(){
Solve();
return 0;
}
当然我不太喜欢这种写法,去倒正负号好麻烦的,其实如果在while上面取mod,也只是一个if判断罢了,while实际上也就执行了一次
如果把mod写在while下面,while确实是发挥了很大作用,跑了好多次,时间直接翻了十倍(狗头)
所以我最喜欢
查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
typedef long long ll;
const int mx=1000;
ll x,y,m,n,l;
ll exgcd(ll a,ll b,ll &x1,ll &y1){
if(b==0){
x1=1;
y1=0;
return a;
}
ll ret=exgcd(b,a%b,x1,y1);
ll t=x1;
x1=y1;
y1=t-a/b*y1;
return ret;
}
void Solve(){
scanf("%lld%lld%lld%lld%lld",&x,&y,&m,&n,&l);
ll a=y-x,b=m-n;
ll x1,y1;
ll an=exgcd(b,l,x1,y1);
if(a%an!=0){
printf("Impossible\n");
return ;
}
x1=x1*(a/an);
x1=x1%(l/an);
if(x1<=0){
if(l/an<0)x1=x1-l/an;
else x1=x1+l/an;
}
printf("%lld\n",x1);
}
int main(){
Solve();
return 0;
}
我们在这个题把一般模型的代码细节的前因后果都讲透了,后面基本就是找出扩欧式然后套代码
倒酒
题意的一个小理解:b酒杯中的酒可以剩下(把a倒满了),下次再给a倒进去
也就是求 ans=by-ax 的一组最小整数解
ans,x,y都为所求
ans,x,y都要输出的话就需要注意x的换号了,一些关于本题的细节都放在代码里了
代码
查看代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
int exgcd(int a,int b,int &x,int &y){
if(b==0){
x=1;
y=0;
return a;
}
int ret=exgcd(b,a%b,x,y);
int t=x;
x=y;
y=t-a/b*y;
return ret;
}
void Solve(){
int a,b;
scanf("%d%d",&a,&b);
// ans=-a*x+b*y 把a的符号给到未知数x上
int x,y;
int d=exgcd(a,b,x,y);
x=-x;a=-a;
//把x换号,x换回来,a得换回去
//我们根据题意已知a,b,d都为正
x=x%(b/d);
y=y%(a/d);
if(x<0 || y<0){
x=x+b/d;
y=y-a/d;
//这里显然两个都是会越来越大
//因为-a本身都是负的,所以同大
}
if(y==0)y=1;//至少得倒一杯
//题库里把 x=x%(b/d);y=y%(a/d),删了不用加这个if
//可以直接A,但是显然,逻辑上说不过去,且会被hack
printf("%d\n%d %d\n",d,x,y);
}
int main(){
Solve();
return 0;
}
荒岛野人Savage
求得一个M,使得任意两个野人之间的
c1 + x*p1 = c2+ x*p2 (mod M) 同余方程都无解
c1-c2 + x*(p1-p2)=m*k
x*(p1-p2) - m*k =c2-c1
不要管同余方程无解是什么意思,关于exgcd,c2-c1除以d不是整数就无解
可以枚举M的值,每一个M,都循环所有的野人判断,n<=15显然是可行的
x的值的倒换还是用的我喜欢的方法,再说句不要脸的,我自创的麻烦法
查看代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
typedef long long ll;
const int mx=30;
struct Node{
int c;
int p;
int l;
}r[mx];
int n;
int exgcd(int a,int b,int &x,int &y){
if(b==0){
x=1;
y=0;
return a;
}
int ret=exgcd(b,a%b,x,y);
int t=x;
x=y;
y=t-a/b*y;
return ret;
}
bool check(int m){
int x,y;
for(int i=1;i<=n;++i){
for(int j=i+1;j<=n;++j){
int a=r[i].p-r[j].p;
int b=m;
int s=r[j].c-r[i].c;
int d=exgcd(a,b,x,y);
if(s%d) continue;//不能整除是无解可以直接跳了
x=x*s/d;
x=x%(b/d);
if(x<=0){
if(d<0)x=x-b/d;
else x=x+b/d;
}
if(x<=r[i].l && x<=r[j].l)return 0;//只要有一对野人会相遇,这个M直接pass
}
}
return 1;
}
void Solve(){
scanf("%d",&n);
int M=0;
for(int i=1;i<=n;++i){
scanf("%d%d%d",&r[i].c,&r[i].p,&r[i].l);
M=max(r[i].c,M);
}
//求得一个M使得同余方程无解,
// c1 + x*p1 = c2+ x*p2 (mod m)
// c1-c2 + x*(p1-p2)=m*k
// x*(p1-p2) - m*k =c2-c1
while(1){
int an=check(M);
if(an==1){
printf("%d\n",M);
return ;
}
M++;
}
}
int main(){
Solve();
return 0;
}