今天在北大POJ上做到一道题——青蛙的约会,连续研究了数个小时,提交了共有50多次,每次都是以wrong anwser或是Time Limit Exceeded被打了回来。后来没吃晚饭,当看到最后一次显示“Accepted”时,内心的焦躁瞬间舒展开来了。
题目原文:两只青蛙在网上相识了,它们聊得很开心,于是觉得很有必要见一面。它们很高兴地发现它们住在同一条纬度线上,于是它们约定各自朝西跳,直到碰面为止。可是它们出发之前忘记了一件很重要的事情,既没有问清楚对方的特征,也没有约定见面的具体位置。不过青蛙们都是很乐观的,它们觉得只要一直朝着某个方向跳下去,总能碰到对方的。但是除非这两只青蛙在同一时间跳到同一点上,不然是永远都不可能碰面的。为了帮助这两只乐观的青蛙,你被要求写一个程序来判断这两只青蛙是否能够碰面,会在什么时候碰面。
我们把这两只青蛙分别叫做青蛙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"
这里可以有两种方法,第一种稍微好理解一些,不过让人感觉诧异的是同样的算法,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"<<endl;
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<<r<<endl;;
break;
}
else
{
int p=z%c+l;
r+=p/c;
z=p%c;
}
if(z==w)
{
cout<<"Impossible"<<endl;
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"<<endl;
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<<p1<<endl;
}
}
return 0;
}
由于题目中给出0 < L < 2100000000,接近int型的上限,在计算步数上时可能出现溢出,所以要使用占用内存8个字节的__int64型,从而保证得数是有效的。