先放题目:
我们把这两只青蛙分别叫做青蛙A和青蛙B,并且规定纬度线上东经0度处为原点,由东往西为正方向,单位长度1米,这样我们就得到了一条首尾相接的数轴。设青蛙A的出发点坐标是x,青蛙B的出发点坐标是y。青蛙A一次能跳m米,青蛙B一次能跳n米,两只青蛙跳一次所花费的时间相同。纬度线总长L米。现在要你求出它们跳了几次以后才会碰面。
Input
Output
Sample Input
1 2 3 4 5
Sample Output
4
其实这道题的题意还是比较好理解的,通俗点说就是两个人在操场散步(同向),每秒走的路程不一样,起点不一样,问过了多少秒之后并肩而行(擦肩而过不算)。
但是写起来就觉得这道题就tmd离谱儿,哪离谱儿呢?数据,2*10^9,正常写是肯定写不了的,这时候就要用到我最近刚学而且最讨厌的数论了,扩展欧几里德定理。
写之前先介绍一下欧几里德算法和扩展欧几里德算法吧。
欧几里德算法又称辗转相除法,用于计算两个整数a,b的最大公约数 gcd(a,b)。基本算法:设 a = qb + r,其中a,b,q,r都是整数,则 gcd(a,b) = gcd(b,r),即 gcd(a,b) = gcd(b,a%b)。
证明:
a = qb + r
如果 r = 0,那么 a 是 b 的倍数,此时显然 b 是 a 和 b 的最大公约数。
如果 r ≠ 0,任何整除 a 和 b 的数必定整除 a - qb = r,而且任何同时整除 b 和 r 的数必定整除 qb + r = a,所以 a 和 b 的公约数集合与 b 和r 的公约数集合是相同的。特别的,a 和 b 的最大公约数是相同的。
递归实现:
1 int gcd(int a, int b)
2 {
3 return b == 0 ? a : gcd(b, a%b);
4 }
非递归实现:
1 int gcd(int a, int b)
2 {
3 while(b)
4 {
5 int t = b;
6 b = a % b;
7 a = t;
8 }
9 return a;
10 }
举个例子吧,24和20,24=4*5+4. gcd(24,20)=4。 gcd(20,24%20)=gcd(20,4)=4,还不懂的在草稿纸上写一写就明白了.
然后就是扩展欧几里德定理,别眨眼,变魔术了。
扩展欧几里得算法就是在求 a,b的最大公约数 m=gcd(a,b) 的同时,求出贝祖等式ax + by = m的一个解 (x,y)。(贝祖等式:ax + by = m 有整数解时当且仅当m是d的倍数。)
有两个数 a,b,对它们进行辗转相除法,可得它们的最大公约数——这是众所周知的。然后,收集辗转相除法中产生的式子,倒回去,可以得到 ax+by=gcd(a,b)的整数解。
先来看下这个几乎所有总结扩展欧几里得算法的帖子中都会用到的例子
(可能出自wikipedia,毕竟wikipedia上也是用的这个栗子):
1 用类似辗转相除法,求二元一次不定方程47x+30y=1的整数解。
2 47=30*1+17
3 30=17*1+13
4 17=13*1+4
5 13=4*3+1
6 然后把它们改写成“余数等于”的形式
7
8 17=47*1+30*(-1) //式1
9 13=30*1+17*(-1) //式2
10 4=17*1+13*(-1) //式3
11 1=13*1+4*(-3)
12 然后把它们“倒回去”
13
14 1=13*1+4*(-3) //应用式3
15 1=13*1+[17*1+13*(-1)]*(-3)
16 1=13*4+17*(-3) //应用式2
17 1=[30*1+17*(-1)]*4+17*(-3)
18 1=30*4+17*(-7) //应用式1
19 1=30*4+[47*1+30*(-1)]*(-7)
20 1=30*11+47*(-7)
21 得解x=-7, y=11。
证明就是根据欧几里德定理gcd(a,b)=gcd(b,a%b)
设 ax1+by1=gcd(a,b)
bx2+(a mod b)y2=gcd(b,a mod b)
则:ax1+by1=bx2+(a mod b)y2
即:ax1+by1=bx2+(a-(a/b)*b)y2=ay2+bx2-(a/b)*by2
根据恒等定理得:x1=y2 ,y1=x2-(a/b)*y2;
这样就可以用x2,y2求x1,y1,直到递归到b=0时结束
这样我们就找到了递推关系:
x1=y2;x1=y2;
y1=x2−(a/b)∗y2;y1=x2−(a/b)∗y2;
递推的终止条件再上面也已经给出:
当b=0,gcd(a,b)=a。此时x=1,y=0;当b=0,gcd(a,b)=a。此时x=1,y=0;
由此我们可以得到递归的扩展欧几里得算法的代码:
1 int exgcd(int a, int b, int &x, int &y)
2 {
3 if(b == 0)
4 {//推理1,终止条件
5 x = 1;
6 y = 0;
7 return a;
8 }
9 int r = exgcd(b, a%b, x, y);
10 //先得到更底层的x2,y2,再根据计算好的x2,y2计算x1,y1。
11 //推理2,递推关系
12 int t = y;
13 y = x - (a/b) * y;
14 x = t;
15 return r;
16 }
比如上面的 47x+30y=1 的例子,exgcd() 要求出 gcd(47, 30),同时得到一组 (x,y)的解。
1 用一个方便我自己理解的方式,把求解的过程写出来好了。这里的大括号"{}"里面不是函数体,"{"表示一层递归调用的开始,"}"表示该层递归的结束。 2 exgcd(47, 30, x, y) 3 { 4 r = exgcd(30, 17,x, y) 5 { 6 r = exgcd(17, 13, x, y) 7 { 8 r = exgcd(13, 4, x, y) 9 { 10 r = exgcd(4, 1, x, y) 11 { 12 r = exgcd(1, 0, x, y) 13 { 14 x = 1; 15 y = 0; 16 return 1(其实是gcd(a,b),不过这里的a,b是30和47互质所以是1了); 17 } 18 t = y = 0; 19 y = x - (4/1) * y = 1; 20 x = t = 0; 21 return r = 1; 22 } 23 t = 1; 24 y = 0 - (13/4) * 1 = -3; 25 x = 1; 26 return 1; 27 } 28 t = -3; 29 y = 1 - (17/13) * (-3) = 4; 30 x = -3; 31 return 1; 32 } 33 t = 4; 34 y = -3 - (30/17) * 4 = -7; 35 x = 4; 36 return 1; 37 } 38 t = -7; 39 y = 4 - (47/30) * (-7) = 11; 40 x = -7; 41 return 1; 42 } 43 最后的结果: 44 r = exgcd(47,30,x,y) = 1; 45 x = -7; 46 y = 11;
介绍完算法了,可以继续自闭之路了,其实这个算法通俗点说就是求出方程的通解,然后再求符合题意的特解。(借用一个脑瘫的话,有点像高中学的参数方程)
首先这道题给了五个数据,设所求时间为t,,所走操场圈数为k,则可以列出(n-m)*t + l*k = x-y。
理解了上面的算法要写出代码就简单了(简单个鸡儿,只是能动手了而已),套用模板求出特解,再判断一下可不可以被l整除,不可以输出Impossible,可以被整除就继续求特解。
这个特解怎么求其实还是要动一下脑子的,首先得知道扩展欧几里德返回的值不是x或y,而是gcd(a,b),然后根据这个值和c也就是(y-x)的关系来找最小非负解。
原式ax + by = gcd(a,b)(这就是我代码里的d)
可以发现两边都除以gcd(a,b)再*c这时右边就是我们要求的c了,但是此时的x并不是我们要求的最小非负数,通过观察可以发现如果(x,y)是该方程的解的话,那么(a-b/d, b+a/d)也是该方程的一组解。
这里可能有细心且耐心的小伙伴就会问了,为什么x要-b/d,好像x-b,b+a也可以成立啊!问得好!
这里又要牵扯出来两个定理了
定理一:若gcd(a, b) = 1,则方程ax ≡ c (mod b)在[0, b-1]上有唯一解。
证明:由上述可知,总可以找到或正或负的整数k和l使a*k + b*l = gcd(a, b) = 1,即我们可以求出ax ≡ 1 (mod b)的解x0。当然,两边乘以c有a(cx0) ≡ c (mod b),所以有x = cx0就是ax ≡ c (mod b)的解。由于加上或减去若干倍b都是该方程的解,所以x在[0, b-1]上有解。那么怎样确定它的唯一性呢?我花了一个小时终于证明出来了,证明方法就是,假设x1和x2都是[0, b-1]上的解,那么就有ax1 ≡ c (mod b),ax2 ≡ c (mod b),两式相减就有a(x1-x2) ≡ 0 (mod b),即a(x1-x2)可以被b整除。但gcd(a, b) = 1啊!所以a和b之间没有共同的语言可以交流,所以只能说(x1-x2)被b整除了。但x1和x2都在[0, b-1]上,所以x1-x2也在[0, b-1]上,所以只能说x1-x2=0了,因此x1=x2。这就证明了解的唯一性!
这个定理不过是为了证明定理二方便而已,定理二才是关键:
定理二:若gcd(a, b) = d,则方程ax ≡ c (mod b)在[0, b/d - 1]上有唯一解。
证明:上面说过,这个该死的方程等价于ax + by = c,如果有解,两边同除以d,就有a/d * x + b/d * y = c/d,即a/d * x ≡ c/d (mod b/d),显然gcd(a/d, b/d) = 1,所以由定理二知道x在[0, b/d - 1]上有唯一解。所以ax + by = c的x在[0, b/d - 1]上有唯一解,即ax ≡ c (mod b)在[0, b/d - 1]上有唯一解,得证!
有了上面几个该死的定理,小爷我终于把最小非负整数的问题解决了。如果得到ax ≡ c (mod b)的某一特解X,那么我令tt = b/gcd(a, b),可知x在[0, tt-1]上有唯一解,所以我用x = (X % tt + t) % tt就可以求出最小非负整数解x了!(X % tt可能是负值,此时保持在[-(tt-1), 0]内,正值则保持在[0, tt-1]内。加上r就保持在[1, 2tt - 1]内,所以再模一下tt就在[0, tt-1]内了)。
这么多证明这么多话语却只换来了这么点代码,这就是我讨厌数论的原因了啊啊啊啊啊啊啊啊啊!!!
为了方便理解我把x的表达式分开来写了。
上AC代码:
1 #include <iostream> 2 #include <cstdio> 3 #include <algorithm> 4 #include <cmath> 5 #include <cstring> 6 #define LL long long 7 using namespace std; 8 LL exoj(LL a, LL b, LL &x, LL &y) 9 { 10 if(b == 0) 11 { 12 x = 1; 13 y = 0; 14 return a; 15 } 16 LL r = exoj(b, a%b, x, y); 17 LL t = y; 18 y = x - a/b * y; 19 x = t; 20 return r; 21 } 22 int main() 23 { 24 LL x, y, m, n, l, a, c; 25 scanf("%lld%lld%lld%lld%lld", &x, &y, &m, &n, &l); 26 a = n-m; 27 c = x-y; 28 if(a<0)//可加可不加,老题数据水,不加也能过,但是加了更严谨 29 { 30 a = -a; 31 c = -c; 32 } 33 LL d = exoj(a, l, x, y);//求gcd(a,b) 34 //printf("temp = %d ", temp); 35 //printf("x = %d ", x);调试用的 36 if(c%d != 0)//不能整除说明怎么跳也不会碰面 37 printf("Impossible "); 38 else 39 { 40 x = x*c/d;//此时方程右边=c 41 LL tt = l/d;//求最小非负数解的取余对象 42 if(x >= 0) 43 x = x%tt;//x为正数时直接取模即可 44 else 45 x = x%tt + tt;//x为负数时取模会得到绝对值最小的负数,再+tt即为最小非负数解 46 printf("%lld ", x);//long long 输出,不然会wa 47 } 48 return 0; 49 }