CC++ & Algorithm

两根绳子,三下除法,就能算出最大公约数——辗转相除法到底妙在哪?

你有没有遇到过这种情况:手里有48颗糖,朋友有18颗糖,想把它们分成几堆,每堆数量一样、必须是整数、而且一颗都不能剩。你可能会从1开始试着除,试到18,发现最大能分成6颗一堆。可如果数字变成几百万呢?你还想一个个试吗?

两千多年前的欧几里得早就想明白了一个道理:与其去“找”公约数,不如把两个数“量”出来。就像用一根长绳量一根短绳,量完剩下的余头,再用短绳去量余头……最后那根能正好量完上一根的绳子,就是答案。


先别急着写代码,理解“为什么能这么做”

给你两根绳子,一根48厘米,一根18厘米。你想找一根最长的小绳,能同时量完这两根。

48里最多有两个18,剩下12厘米。现在问题变成了:18和12的公约数,是不是和48和18的公约数完全一样?

答案是肯定的。因为48 = 18×2 + 12,所以任何能同时整除48和18的数,一定能整除12;反过来,任何能同时整除18和12的数,也一定能整除48。公约数集合没变,最大公约数自然也没变。

于是我们把求 gcd(48, 18) 转化成了求 gcd(18, 12),数字变小了,问题却没变。继续:

  • 18 = 12×1 + 6
  • 12 = 6×2 + 0

余数为0,最后那个非零除数6就是答案。整个过程不过三次除法。

这也是辗转相除法最迷人的地方:每做一次除法,问题规模就急剧缩小。余数一定小于除数,而更关键的是,余数通常连除数的一半都不到(想想为什么?因为如果余数超过一半,商就可以再加1了)。所以对于百万级别的数,最多也就二十来步。


代码就几行,但别轻视它

递归写法最直观:

int gcd(int a, int b) {
    if (b == 0) return a;
    return gcd(b, a % b);
}

循环写法更省栈空间:

int gcd(int a, int b) {
    while (b != 0) {
        int r = a % b;
        a = b;
        b = r;
    }
    return a;
}

有个新手经常踩的坑:总想先交换两数,保证a >= b。其实完全没必要。如果a < b,第一次 a % b 的结果就是a本身,然后赋值后自动交换了角色。多写那个 swap 不算错,但这种“多余的谨慎”往往来自没有真正理解算法本身。

真正要注意的是输入不能两个都是0,否则第一次取模就崩了。一般题目里都会保证正整数,但如果自己做测试,记得加个判断。


一道“素数个数”题,居然也能和它扯上关系?

有个题目问你:1到N之间有多少个素数?N最大能到一亿。你可能会想:这跟最大公约数有什么关系?

直接判断每个数是不是素数,复杂度是O(N√N),一亿根本跑不动。但你发现没有——求素数个数,本质上是在做排除法。而排除的核心,是“用素数筛掉倍数”。怎么快速判断某个数是否已经被筛掉?依然离不开整除操作。虽然这题真正的高效解法是欧拉筛或埃氏筛,但在实现筛法时,你同样需要频繁用到“整除”和“余数”的概念。

辗转相除法教你的是:余数不是垃圾,它是缩小问题规模的钥匙。在编程竞赛里,很多优化都源于对余数的巧妙利用。比如判断一个数能否被某个素数整除,本质就是看 n % p == 0。理解了余数的意义,再看“筛法优化”“哈希取模”“循环节”这些问题,你会比别人多一层敏感。

那道最大比例的题目就更是直接了。给你几个数,它们构成一个等比数列,但中间有缺失,让你反推最大的公比。你会得到一堆分数,比如4/3、16/9、8/3……怎么找到它们共同的最大“比例公约数”?普通除法办不到,因为你要找的不是一个数能同时整除它们,而是一个分数能同时“整除”这些分数。

怎么办?把分子分别求最大公约数,分母分别求最小公倍数。最终得到的分数就是这些比例的最大公比。你再仔细品品——这里求分子最大公约数用的,还是辗转相除法。同一个算法,换了一层皮,就解决了分数比例问题。


聊聊这个算法的“底层气质”

很多人学算法只记代码,不看思路,遇到变体就懵了。辗转相除法的变体很多:

  • 求最大公约数可以扩展到多个数:两两求,最终合并。
  • 求最小公倍数只需要 a / gcd(a,b) * b,注意先除后乘防止溢出。
  • 扩展欧几里得算法能解 a*x + b*y = gcd(a,b),后面的逆元、RSA加密全都要用到它。

说白了,辗转相除法之所以重要,不是因为代码短,而是它展示了一种思维范式:把一个看似复杂的问题,通过“取余”操作快速降维,直到问题自然显形

你以后会遇到二分、快速幂、gcd的递归写法……这些算法的核心都带着同样的味道——每一步都在缩小问题,但答案始终不变。你只要抓住了“不变性”,代码怎么写都不会错。


所以下次你看到 while(b) swap(a%=b, b); 这种一行代码时,别只觉得帅。它背后是一个用绳子量了两千年的朴素智慧。如果你真的理解了,那以后不管数字多大,也不过是几行除法的事。

关于作者

我是赵老师,持有 NOI 信息学奥赛教练证书,拥有 15 年以上的软件开发经验,从事信息学少儿编程教学已有 8 年时间。

这些年累计帮助 多名 学生通过编程特长升入自己心仪的目标学校。

如果你在编程学习上有任何疑问,欢迎联系我:18620372957(微信同号)

这篇文章对你有帮助吗?

成为第一个评价的人

想系统学习这个知识点?查看完整知识点 →