在森林的深处,有一只热爱烘培的小鸽子,她每天过着无忧无虑的生活。在白天,太阳带给她光芒,绿叶带给她阴凉;而晚上,星星和月亮变成了她的夜灯。有时,她在灌木间飞翔,与蝴蝶共舞;时而,她落在河边,对着天空发呆。她不知道,那些蝴蝶从何而来;又不知天上的云向何而去。她知道的,只有林中清新快乐的气息,和自己烘培面包的香味。
一天,一只爱唱歌的百灵鸟来到了森林中,和小鸽子相遇——他们很快就成了最好的玩伴。他们一起歌唱,一起飞翔,一起在林间谈着对未来的畅想。一天偶然间,小鸽子听说了百灵鸟喜爱面包的清香,于是她准备把自己珍藏和他一起分享。
而后,小鸽子得知,百灵鸟只喜爱正方形的面包,而且愈大愈佳。可她的面包多以长方形为主,故而只得将其切成正方形。此外,因为小鸽子刀艺不精,只得将面包按整厘米数切开,无法切成几分之几厘米。同时,因为小鸽子对自己面包颇为珍惜,不希望切去之后仍有残留。
现在,小鸽子有一块Mcm✖Ncm的面包,请问她要怎样切,才能在满足百灵鸟需求的基础上没有剩余,而且一块尽可能大呢?
咳咳,言归正传,这道题就是问有一个M*N的长方形,问怎么恰好分成若干个相同大小的正方形,且越大越好。我们拿一个6*9的长方形举例。
如果想要分成尽可能大的等大的,边长为整数厘米的正方形,并且没有剩余,那么很明显是6个3×3。
现在学过一点点数论的同学应该就很快发现了。(没学过数论的我也帮你发现好了)这其实就是求9和3的最大公约数。
等等!啥是最大公约数?最大公约数(Greatest Common Devisor)是指两个(或多个)正整数中,共同拥有的最大约数(或者叫因数)。约数就是指能被一个数整除,也就是除以它余零的数。比如说12的约数有1、12、2、6、3、4。16的约数有1、16、2、8、4。那么如果要问12和16的公约数,那就是在刚才各自列举的约数中找共同的且最大的——很明显就是4了。所以我们可以这样表示:gcd(12,16)=4.
回到分蛋糕的问题,其实问题本质就是求长方形长和宽的最大公约数。因为要保证是整数厘米的正方形边长并且没有剩余,说明这个边长一定能被长方形的长和宽除尽,所以一定是它们的公约数。而又要求这个正方形尽可能大,所以就是他们的最大公约数了!
那么问题来了——这个最大公约数怎么求呀?其实在介绍它的定义时,我已经给出了一种求法:列举法。就是把两个数各自的所有约数都列举出来,从里面挑一个最大的就可以了。这种方法也很好用代码实现:
#includeusing namespace std;int factor1[10010],factor2[10010];//两个数组分别纪录两个整数的因数int main(){ int n,m; cin>>n>>m; for(int i=1;i<=n;i++) if(n%i==0) factor1[i]=1; for(int i=1;i<=m;i++) if(m%i==0) factor2[i]=1; for(int i=min(m,n);i>=1;i--) if(factor1[i]&factor2[i]){//如果一个数是共同约数 cout<//输出就好啦 break; } return 0;}
当然,这种算法属实很慢,它的时间复杂度是O(n). 而在一般情况下,m、n的值都会很大很大,而且,为了纪录约数,还要开很大的数组,十分麻烦。因此,我们需要想想怎么优化。
这就涉及到了我们今天所要讲的“欧几里得定理”
学过小学奥数的同学们肯定都听说过一种方法,就是“辗转相除法”,而辗转相除法的根本就是欧几里得定理:
a和b的最大公约数等于b和a除以b的余数的最大公约数。
或者这样表述:
gcd(a,b)=gcd(b,a%b)
然后我们只需要根据这个定理,不断重复这个操作,来求最大公约数。举个例子吧,我们要求177和237的最大公约数。也就是gcd(237,177)。根据等式,我们只需要求gcd(177,60),这里面60就是237÷177=1...60这个余数。然后,我们继续求gcd(60,57),同样的,这里的57就是177÷60=2...57这个余数。然后,我们求gcd(57,3)。这时,我们发现57是3的倍数。因为我们直到两个数中,如果一个数是另一个数的倍数,那么他们的最大公约数就是较小的那个。因此这时候,我们就知道了gcd(57,3)等于3.那么再推回去,gcd(237,177)也就等于3了。
等一下,为什么这个公式是正确的呢?
我们把证明分为两步骤:
1、证明gcd(a,b)是b,a%b的一个公约数
2、证明这个公约数是最大的。
一、
我们设gcd(a,b)=d,再令a=k1×d,b=k2×d.
我们再设,a=k×b+c(也就是a除以b商k余c),那么c就是余数,也就是a%b.
将上面那个式子移项,得到c=a-k×b,然后再把a=k1×d,b=k2×d,这两个式子里的a、b带入式子,得到:
c=k1×d-k×k2×d,在提取公因数d,得到c=(k1-k×k2)×d.这样就说明,c,也就是a%b有d这个约数,因为开始我们设b也有d这个约数,所以gcd(a,b)是b,a%b的一个公约数。
二、
现在知道了它是一个公约数,那么怎么证它是最大的?(其实感性分析,a%b都变小了,公约数不可能更大呀!)
但是数学是一门严谨的学科,我们要严谨证明。我们知道,c(a%b)=(k1-k×k2)×d,b=k2×d,我们只需要证明k1-k×k2、k2互质就好了。
这里可以用到反证法:我们假设k1-k×k2=q×t,k2=p×t,并且t>1(也就是那两个不互质)。
我们将前面那个式子移项,得到k1=q×t+k×k2,再把这个k1代到最开始的a=k1×d,得到a=(q×t+k×k2)×d,再利用乘法分配律,得到:
a=q×t×d+k×k2×d,我们这时发现,k2×d不就是最开始的b吗?,将其带入,得到:a=q×t×d+b×d.
这时,我们再把k2=p×t代入开始的b=k2×d,得到b=p×t×d,再把这个式子代到a=q×t×d+b×d.得到了:a=q×t×d+p×t×d.提取公因数:a=(q+p)×t×d
现在,再和b=p×t×d比较,发现他们的最大公因数变成了t×d和开始矛盾,所以假设不成立,反证成功!
当然证明欧几里得定理的方法还有很多,在这位数学家名著《几何原本》中,也有用几何对其证明的方法。这里也就不一一列举了。那么既然完成了证明,我们怎么用程序实现这个算法呢?
如果要不断地进行辗转相除,最容易想到的就是递归,不断地自己调用自己。那么什么时候结束呢?只要进行到gcd(a,b),当b等于0时,就说明上一步中前面一个数是后面一个数的倍数,所以答案就是上一次调用的后面一个数,也就是这里的a。把a返回就好了。
好了,代码如下:
#includeusing namespace std;inline int gcd(int a,int b){ if(b==0) return a;//找到答案,返回 return gcd(b,a%b);//递归调用函数}int main(){ int a,b; cin>>a>>b; cout<}
以上就是欧几里得求最大公约数的算法了。比起传统的列举法,在不占用庞大的数组空间的基础上,时间复杂度也优化成了O(logn),十分快速。
欧几里得算法是信息竞赛数论板块的基础,在此之上还有扩展欧几里得等更为复杂的算法,如果大家对此感兴趣,别忘关注我们的公众号哦!