【算法】欧几里得算法与青蛙约会oj
欧几里得和扩展欧几里得算法
题目:
- poj 1061
- poj 2142
- 双六
扩展欧几里得算法详解
先说欧几里得算法:欧几里得算法辗转相除求\(gcd\)。求\(a、b\)的\(gcd\),则利用的性质是:\(gcd(a,b)=gcd(b,a\%b)\),而\(gcd(a,0)=a\),这样,辗转除下去,当第二个参数为0,第一个参数就是最大公约数。
int gcd(int a,int b){
while(b!=0){
int tmp = a%b;
a = b;
b = tmp;
}
return a;
}
扩展欧几里得算法:扩展欧几里得算法不仅可以用来求最大公约数,还可以求逆元/满足ax+by=gcd(a,b)的x和y。基于的原理是:ax+by=gcd(a,b)一定存在解(x,y)。
一个用处就是:问ax+by=1是否有解,就是看a,b是否互质,即gcd(a,b)=1。
求满足ax+by=gcd(a,b)的(x,y)的过程,就是证明ax+by=gcd(a,b)一定有解的过程。
\(ax+by=gcd(a,b)=gcd(b,a\%b)=bx_2+a\%by_2\)
又 \(a\%b=a-a/b*b\)
因此 \(ax+by =bx_2+(a-a/b*b)y_2 =ay_2+b(x_2-a/b*y2)\)
从而 \(x = y2;y=x_2-a/by_2\)
层层递归下去,最终当\(b=0\)时,返回\(gcd(a,b)=a\),此时有一组解\(x=1,y=0\),回溯,利用上式求出\(x,y\)。
从而,这就建立了要求的(x,y)和前一状态的关系,算法的递归实现如下:
int exgcd(int a,int b,int &x,int &y){
/*输入ax+by=gcd(a,b)的a,b,返回gcd(a,b),同时求出满足此式的解(x,y)*/
if(b==0){
x = 1;
y = 0;
return a;
}
int p = exgcd(b,a%b,x,y);
int tmp = x;
x = y;
y = tmp - a/b*y;
return p;
}
int main(){
int x,y;
int gcd = exgcd(24,18,x,y);
printf("24*%d + 18*%d = %d\n",x,y,gcd);
return 0;
}
继续.....
仅仅知道欧几里得解\(ax+by=gcd(a,b)\)还不够,扩展欧几里得最好用的地方在于其求解乘法逆元。所谓逆元:若\(ax==1mod(m)\),则称\(x\)是\(a\)关于\(m\)的逆元,逆元可能有许多个,求的是其中最小的一个。
\(ax==1mod(m)\) \(=>\) \(ax+my=1\)有解,求此式的解中最小的\(x\)即可。即求出此式\(x\),\(x=x\%m\),如果\(x<0\),\(x+=m\)。
求解代码借助exgcd:
/*求逆元模板*/
int exgcd(int a,int b,int &x,int &y){
if(b == 0){
x = 1;
y = 0;
return a;
}
int p =exgcd(b,a%b,x,y);
int temp = x;
x = y;
y = temp - (a/b) *y;
return p;
}
//求逆元 求a关于m的最小逆元
int cal(int a,int m){
int x,y;
int gcd = exgcd(a,m,x,y);
if(1%gcd != 0) return -1;
x *= 1/gcd;
int ans = x % m;
if(ans<=0) ans+=m; //保证正的且最小
return ans;
}
/*另一个简洁版本的求逆元*/
void exgcd(int a,int b,int &x,int &y){
if(b == 0){
x = 1;
y = 0;
}else{
exgcd(b,a%b,y,x);
y = y - (a/b)*x; //此处应注意
}
}
例题:青蛙约会poj 1061
代码解释:https://blog.csdn.net/liangdong2014/article/details/38732745
poj 1061 青蛙约会 |
---|
两只青蛙在网上相识了,它们聊得很开心,于是觉得很有必要见一面。它们很高兴地发现它们住在同一条纬度线上,于是它们约定各自朝西跳,直到碰面为止。可是它们出发之前忘记了一件很重要的事情,既没有问清楚对方的特征,也没有约定见面的具体位置。不过青蛙们都是很乐观的,它们觉得只要一直朝着某个方向跳下去,总能碰到对方的。但是除非这两只青蛙在同一时间跳到同一点上,不然是永远都不可能碰面的。为了帮助这两只乐观的青蛙,你被要求写一个程序来判断这两只青蛙是否能够碰面,会在什么时候碰面。 我们把这两只青蛙分别叫做青蛙A和青蛙B,并且规定纬度线上东经0度处为原点,由东往西为正方向,单位长度1米,这样我们就得到了一条首尾相接的数轴。设青蛙A的出发点坐标是x,青蛙B的出发点坐标是y。青蛙A一次能跳m米,青蛙B一次能跳n米,两只青蛙跳一次所花费的时间相同。纬度线总长L米。现在要你求出它们跳了几次以后才会碰面。 |
\(Input:\) 输入只包括一行5个整数x,y,m,n,L,其中x≠y < 2000000000,0 < m、n < 2000000000,0 < L < 2100000000。 |
\(Output:\) 输出碰面所需要的跳跃次数,如果永远不可能碰面则输出一行"Impossible" |
Sample Input
1 2 3 4 5
Sample Output
4
思路:
在一个周长是\(L\)的环上,A青蛙处于\(x\)处,每次跳\(m\),B青蛙处于\(y\)处,每次跳\(n\),跳相同的步数后,两青蛙处于同一位置。由此,设跳\(t\)步相遇,则:
\((x+m*t)\%L=(y+n*t)\%L\)
\(=>(m-n)*t\%L=(y-x)\%L\)
\(=>(m-n)*t\%L=y-x\)
若此式有解,则应有,
\(=>(m-n)*t_1+L*t_2=y-x\)有解,由exgcd知,有解的条件是
\(=>(y-x) \% gcd(m-n,L) =0\)
若有解,下面求解满足条件的最小\(t_1\),进而推出\(t\)。
由exgcd求出满足\((m-n)*t_1+L*t_2=gcd(m-n,L)\)的一组解:
exgcd(m-n,L,t1,t2);
由于\(y-x\)是\(gcd(m-n,L)\)的整数倍,则\((m-n)*t_1+L*t_2=y-x\)的一个解是:
\(=>t_1=t_1*(y-x)/gcd(m-n,L)\);
注意这样求出来的\(t_1\)可能并不是最小,因此对\(t_1\)做如下操作:
\(=>t_1=t_1\%L\)变为最小;
但\(t_1\)还可能是负值,因此,变为正的:
\(=>if_{ t_1<0}:t_1=t_1+L;\)
从而解出了\(t=t_1\)。
实现代码:
#include<iostream>
using namespace std;
typedef long long ll;
ll x,y,m,n,L;
ll gcd(ll a,ll b){
while(b!=0){
ll tmp = a%b;
a = b;
b = tmp;
}
return a;
}
void exgcd(ll a,ll b,ll&x,ll &y){
if(b == 0){
x = 1;
y = 0;
}else{
exgcd(b,a%b,y,x);
y = y - (a/b)*x;
}
}
int main(){
ll a,b,c,d,t1,t2,L2;
while(scanf("%lld%lld%lld%lld%lld",&x,&y,&m,&n,&L)!=EOF){
a = m-n;
b = y-x;
d = gcd(a,L);
if(b % d != 0){
printf("Impossible\n");
}else{
exgcd(a,L,t1,t2);
t1 = t1*b/d; //先变换到求at1+Lt2=y-x,再求最小,不能先求最小再变换,否则题意不符
t1 = t1 % L;
if(t1<0){
t1 += L;
}
printf("%lld\n",t1);
}
}
return 0;
}