今天在北大POJ上做到一道题——青蛙的约会,连续研究了数个小时,提交了共有50多次,每次都是以wrong anwser或是Time Limit Exceeded被打了回来。后来没吃晚饭,当看到最后一次显示“Accepted”时,内心的焦躁瞬间舒展开来了。 题目原文:两只青蛙在网上相识了,它们聊得很开心,于是觉得很有必要见一面。它们很高兴地发现它们住在同一条纬度线上,于是它们约定各自朝西跳,直到碰面为止。可是它们出发之前忘记了一件很重要的事情,既没有问清楚对方的特征,也没有约定见面的具体位置。不过青蛙们都是很乐观的,它们觉得只要一直朝着某个方向跳下去,总能碰到对方的。但是除非这两只青蛙在同一时间跳到同一点上,不然是永远都不可能碰面的。为了帮助这两只乐观的青蛙,你被要求写一个程序来判断这两只青蛙是否能够碰面,会在什么时候碰面。 Input 输入只包括一行5个整数x,y,m,n,L,其中x≠y < 2000000000,0 < m、n < 2000000000,0 < L < 2100000000。 Output 输出碰面所需要的跳跃次数,如果永远不可能碰面则输出一行"Impossible"
这里可以有两种方法,第一种稍微好理解一些,不过让人感觉诧异的是同样的算法,C++实现的结果是 Time Limit Exceeded,而换做C语言却通过了对POJ的提交。这也就不得不说明C语言虽然功能更为单调,但是由于更接近底层操作,它的运行速度仍是一大优势。
方法一分析: 两只青蛙的初位移差:x-y,跳跃能力差值m-n。简单的相对性的原理,我们便可以假设是一只青蛙在跳,跳跃能力是m-n;初始位置x-y;即理解为一只青蛙从x-y位置开始,以m-n的跳跃能力,能否在若干次跳跃后回到0点上。分析一下下就会想到,如果不能,这只青蛙将会以某种规律在除原点外的一些点上循环跳下去。那么我们就以青蛙每过一次跳经原点后距离远点大小为标记,如果有一次恰好跳在原点上,那么就得出了答案,如果若干次后,它不但没有跳到原点上,反而又跳到了出发位置上,那么说明它将以此规律无限循环跳下去而永远也跳不到原点上,这时就可以大胆输出“Impossible”了。下面是用C++将其实现的代码: #include
using namespace std;
int main() { int x,y,m,n,l; cin>>x>>y>>m>>n>>l; if(m==n) { cout<<"Impossible"< return 0; } else { int c,z; if(m>n) { c=m-n; z=(y-x+l)%l; } else { c=n-m; z=(x-y+l)%l; } int w=z%c; int r=z/c; while(true) { if(z%c==0) { cout< break; } else { int p=z%c+l; r+=p/c; z=p%c; } if(z==w) { cout<<"Impossible"< break; } } } return 0; } 可惜的是这个代码提交后显示程序运算超时了(C语言翻译后居然可以通过,很令人更喜欢C++的自己吐槽。。。),虽然算法好理解,但是似乎是把更多的运算与判断都交给了计算机,貌似还得另寻途径。
下面方法2是理解上比上面更复杂点的解决办法,毕竟要给计算机省时省力,还得多用人脑细胞吧。 方法二分析:要另两只青蛙相遇,必须是累计跳跃布数是L的整数i倍,另跳k步,即(k*m+x)-(k+n+y)=L*i (i=0,1,2,3......)。简单整理后:L*i+(n-m)*k=x-y;其模型就是a*i+b*k=t(a,b,t都是已知整数)关于i和k是否有整数解。那好,先把这个问题放一下,现在引入一个很关键的数学性质:就是如果a,b如果是互质的,那么他们的线性组合可以得到任意的整数,证明如下:我们先证明a*i+b*k可以等于1,那么就能说明可以等于任意整数(直接乘以相应倍数就可以)。设n是a与b线性组合可以得到的最小的正整数。令m是另外一个可以线性组合得到的数:m=f*n+r(0<=r);那么f*n也是属于a,b线性组合可以得到的,那么r就也是了;所以r就只能是0了。这样的话我们就得到a,b线性组合可以得到的数的集合{p}中所有的整数都是最小的n的倍数。我们另互质的a,b线性组合得到的最小的数仍是n,那么 a*x0+b*y0=n; 又有:a*(x0+1)+b*y0=n+a; a*x0+b*(y0+1)=n+b; 所以n+a和n+b都是n的倍数,条件中a,b互质,那么n的值就只能是1了; 证到这里,我们把得到的结论总结一下: 如果a,b互质,那么这道题肯定有解的(确定不会输出“Possible”了),如果a,b不互质,那么如果t中含a,b最大公约数w,这时都除以w,仍然可以说明不用输出“Possible”了。 所以程序首要任务就是算出a,b的最大公约数。对于此,我们可以用欧几里得的辗转相除法则,即a,b的最大公约数等于b与a%b(%是取余符号)的最大公约数。 关于辗转相除的正确性,为了一窥其本质,试图证明如下: 设a,b的最大公约数为n; 另不妨令a=k*b+c;b=g*n;那么c一定也是n的倍数了,即n也是b与c的最大公约数。所以辗转相除的正确性就证明了。 求最大公约数,根据上述原理,其C++的实现如下: int getMostCommenDivisor(__int64 a,__int64 b) { if(b==0) { return a; } return getMostCommenDivisor(b,a%b); }
然后判断得到的最大公约数是否能被等式左边的t整除。然后怎样算出准确的跳跃步数就是下一个任务了; 关于a*i+b*k=t的解,等效于计算(a/n)*i+(b/n)*k=t/n 的解。有一个办法,就是暴力算法(穷举),从0开始,两个for循环算下去,但是这样转了一大圈还不如第一种方法来得省计算机资源。所以得找其他办法; 其实我们可以容易证明:a*i+b*k = b*i0+(a%b)*k0 = a*k0+b*(i0-(a/b)*k0); 即i=k0,k=i0-(a/b)*k0; 递归的方法可很好的实现这个算法: void getAnwser(__int64 a,__int64 b,__int64 &x,__int64 &y) { if( b==0 ) { x=1; y=0; return; } getAnwser(b,a%b,x,y); __int64 w=x; x=y; y=w-a/b*y; return; } 而(a/n)*i+(b/n)*k=t/n 的解一定是有无数组的,方程解如下: 其中t为任意整数 i = t/n* i0 + b/n * t k = t/n * k0 - a /n* t 。 这也可以理解为,如果两只青蛙能够相遇,继续跳下去的话,他们将能够相遇无数次。 而我们要的答案是首次相遇时所跳的步数,即我们要求的大于零且最小的那个i的解,这在程序中也是必须要体现的。说到这里,所有的要解决的算法都已分析完,完整的C++代码如下:
#include using namespace std;
void getAnwser(__int64 a,__int64 b,__int64 &x,__int64 &y) //计算初解的函数 { if( b==0 ) { x=1; y=0; return; } getAnwser(b,a%b,x,y); __int64 w=x; x=y; y=w-a/b*y; return; }
int getMostCommenDivisor(__int64 a,__int64 b) //得到最大公约数的函数 { if(b==0) { return a; } return getMostCommenDivisor(b,a%b); }
int main() { __int64 x,y,m,n,l,a1,a2,a3,r; while(cin>>x>>y>>m>>n>>l) { a1=n-m; a2=l; a3=x-y; r=getMostCommenDivisor(a1,a2); if(a3%r!=0) //判断是否能被整除,从而知道能否相遇 cout<<"Impossible"< else { __int64 p1,p2,t; a1=a1/r; a2=a2/r; a3=a3/r; getAnwser(a1,a2,p1,p2); p1=a3*p1; while(p1>0) //我们要得到的是最小的那个正整数p1 p1-=a2; while(p1<0) //保证是刚刚大于零的那个最小p1 p1+=a2; cout< } } return 0; }
由于题目中给出0 < L < 2100000000,接近int型的上限,在计算步数上时可能出现溢出,所以要使用占用内存8个字节的__int64型,从而保证得数是有效的。 |