扩展欧几里得_客户端不用的算法系列:从头条笔试题认识扩展欧几里得算法

难度较高,阅读时间大概 28 分钟

c5461f42b20df4192ac98c98cc4bcadc.png

这是数论的第二篇,在《素数筛法》中,我们重温了素数这个数学定义,并且给出了区别于教科书上更高效的 Eratosthenes 筛法欧拉线性筛。这篇文会从 GCD 问题出发,一起来探究一下扩展欧几里得算法。

看了标题,也许你有疑问,什么是欧几里得算法?欧几里得算法是为了解决 GCD 问题,这里的 GCD 是指 Greatest Common Divisor 即 最大公约数,而不是 iOS 中的 Grand Central Dispatch ? 。所以这篇分享是关于算法的。

欧几里得算法(GCD)

求 GCD 在数论中公认的最常用算法即为欧几里得算法,也就是我们在高中时学到的辗转相除法

欧几里得算法的基本原理用一句话就可以说清楚:两个整数的最大公约数等于其中较小的数和两数的差的最大公约数,即 gcd(a, b) = gcd(b, a mod b)

为什么可以这么求呢,这里可以简单证明一下:

假设 a, b (a > b) 两个数的一个公约数是 t ,则有

581d4b1ba12d82f0c2476b6554689ea7.png

因为 a > b ,设 a = k × b + r ,即 r = a mod b ,将 ab 代入展开可得:

3f2f9e36443ab16db3bec964acfc5123.png

由于 (n - k × m) × t 一定是整数,所以 ab 的公约数 t 也是 r 的约数。所以如果我们递归的求解 a mod b 也就是 a % b ,就可以得到 ab 的最大公约数 GCD 了。什么时候递归结束呢?当 a % b == 0 的时候,因为在这个过程中,如果 a mod b 无法求得正整数 r 时,则无法继续按照上述规律继续拆分。

    # pythondef gcd(a, b):        return a if b == 0 else gcd(b, a % b)
// Cint gcd(int a, int b) {        return b == 0 ? a : gcd(b, a % b);}

这里另外提一句,ab 两数的最大公倍数 LCM(a, b) = a * b / GCD(a, b) 。这里就不证明了,有兴趣的自己谷歌。

一道头条的笔试题

上个月在脉脉上看到一道头条校招的笔试题,看评论说是“地狱难度”的,我们通过这道题来延伸说一下。先来看下这题的题面:


有一台用电容组成的计算器,其中每个电容组件都有一个最大容量值(正整数)。对于单个电容,有如下操作指令:

指令1:放电操作-把该电容当前电量值清零;

指令2:充电操作-把该电容当前电量补充到最大容量值;

指令3:转移操作-从电容 A 中尽可能多的将电量转移到电容 B ,转移不会有电量损失,如果能够充满 B 的最大容量,那剩余的电量仍然会留在 A 中。

现在已知有两个电容,其最大容量分别为 ab,其初始状态都是电量值为 0,希望通过一些列的操作可以使其中某个电容(无所谓哪一个)中的电量值等于 c (c也是正整数),这一些列操作所用的最少指令条数记为 M,如果无论如何操作,都不可能完成,则定义此时 M = 0

显然对于每一组确定的 a,b,c,一定会有一个 M 与之对应。

这里需要输入的是 abc ,给出两个样例,例如 a = 3, b = 4, c = 2 ,则最少需要 4 个指令完成。


解释:设最大容量为 3 的是 A 号电容,另一个是 B 号电容,对应的操作是 (充电 A)=> (转移 A -> B) => (充电 A)=> (转移 A -> B) ,这样 A 就是目标的 2 电量。

第二个样例 a = 2, b = 3, c = 4,由于 a 和 b 都无法到目标电量 4,所以输出 0 代表无解。

这道题我们拿到以后,第一反应就是模拟三个指令,然后使用 BFS 广度优先搜索来搜出答案,只要任意情况到达目标的 c 值就停下来。但是题目中给出了数据量 0 < a, b, c < 10^9 ,这个数据量约束了我们无法使用暴力搜索来求解。

简要分析

首先从笔试的角度来分析,由于笔试时会有数据范围的测试,这道题给出的数据范围大概是这样:

0 c 10^0 c 10^0 c 10^

所以如果没有任何的思路和数论基础,我建议使用 BFS 直接写一版暴力,最少可以通过 > 50% 的数据,从而拿到一定的分数。(其实这就是 OI 得分赛制,没有思路先暴力抢分)。

下面我们来分情况讨论这个问题:

情况一

样例已经给出了一种边界情况,即当 c > max(a, b) ,这种情况是无法使得 A 和 B 的电量达到 c 的。直接输出 0。

情况二

还有一种我们可以直接想到的情况,当 a = c 或者 b = c 的时候,只进行一次充电操作就可以完成,直接输出 1。

情况三

接下来我们考虑一般情况,即需要满足以下前提条件:

63a54edb0d32e633e6f9a737cb6b6645.png

我们将这个问题换一个思路转化一下假设给出的 abc 一定有解,那么我们来设置对 A 做了 x 次的充(放)电,对 B 做了 y 次的充(放)电,并且做了 k 次的操作三。如果将 A、B 当做一个大电容来看这个电容只有充放电 a 单位、充放电 b 单位这 4 种操作。那么我们就可以列出一个关系式:

20fd4e6b98cf772c8e2c358a8a432160.png

由于 ab 为非负整数,又因为前提条件 c < max(a, b) ,则 xy 符号相反。

暂且,我们先不管做了几次操作三,先只考虑充放电问题,那其实就是已知 abc,我们在给定范围内求解 xy 的解就可以了。那么这个问题我们要如何求解呢?这就是扩展欧几里得算法所要解决的问题

扩展欧几里得算法(Extended Euclidean)

* 的章节略有难度。如果是从解决问题的工程角度出发,可以跳过证明直接记结论。

在推导上述问题的求解算法之前,我们需要先了解以下几个概念知识。

丢番图方程(Diophantine Equation)

丢番图方程指的是:未知数个数多于方程个数,且未知数只能是整数的整数系数方程或方程组。例如以下式中,a、b、c 都为整数:

bc8fdd31956bc8c2f31d51acbd334f03.png

关于代数学鼻祖丢番图(Diophantus)除了有《算数》这本开山巨作之外,还有一个好玩的数学题目墓志铭,有兴趣可以自己了解。

裴蜀定理(Bézout's identity)

在数论中,裴蜀定理是一个关于最大公约数的定理。这个定理说明了对于任意整数 a、b 和他们的最大公约数 d,关于未知数 xy 的线性丢番图方程:

3242af14ff91f2fe05bae77bc1cf9ce1.png

有解,当且仅当 md 的倍数时。这个等式也被称为裴蜀等式。

裴蜀等式有解时必然有无穷多个整数解,每组解 xy 都称之为裴蜀数,可用辗转相除法求得。

辗转相除法实现扩展欧几里得算法

既然说可以用辗转相除法来解决这个问题,那么我们先来说明一下如何通过辗转相除法来求二元一次线性丢番图方程。

辗转相除法过程

23x + 17y = 1 为例,我们来求 GCD(23, 17)

15558020ad778926d61c988f9e87f3ba.png

改写成余数形式

将等式右边的第一项移项:

6f15520c21da6bad20ed0933bbed9946.png

反向带入原式

带下划线的 65 会使用 (1)(2) 两个式子反向带入,形同换元:

6900871d625b20e6bb10b8bf7fa8745d.png

所以反解得,x = 3, y = -4 是上述二元一次线性丢番图方程的一组解。

* 扩展欧几里得算法证明

来观察一下辗转相除法的最后两个式子,终止条件是:

4f6fd9732efad50c45f859f7f0686e98.png

当且仅当第二个式子为 0 的时候停止这个递归运算。如何延伸到一般情况呢?我们将待求变量设为字母来尝试一下。假设此时,我们要求 anbn 为系数的二元一次线性丢番图方程的系数,即待求方程:

fc64d590a17ced0c9268fb75d75c5c98.png

根据上述的改写余数形式,我们可以列出式一(| 是整除的意思):

fe92110d6a671023724d6bc8cf64dd65.png

假设未到达最终的终止条件,则有:

b19d8b1f6ea1983255bc3b4f3571e24f.png

第二个式子中我们可以发现:

d1aa5efef69e2071c11b63fac2c5cbbf.png

同理,第 n 个式子中有:

724c97a5d3c16660c4093c03a891aae7.png

根据辗转相除的规则,我们知道第 0 项中 b = 0a = 1 ,而我们要求的是第 n 项中的 ab,所以可以通过 ab 的递推公式逐一推导而来。

如此我们证明了 anbn 的递推关系,下面我们来证明 xn 的递推关系。

ee7f8a20e92949e87b86e19191d78453.png

由上文证得了:

daa0385734b2ce7856850b975113dab2.png

我们将其带入到第一个式子中:

7749b816ff31b93efe2a0a22c1fb9492.png

所以可以求得:

6a9bb2e8597f17037352eeb03682e21f.png

由于辗转相除的推论我们可得:

1e6bbb5cdc7849a5b838237b581b39e2.png

所以:

7e82ac6ea235cbbf5ce9569ad1392117.png

即:

9a6beea4a9df4bf2a2ccf1c5ccbae560.png

代码实现扩展欧几里得算法

为了实现上述的反向带入原式的过程,我们通过递归递归到最深的一层,将每一层的解带入即可完成最终的求解:

# pythondef ex_gcd(a, b):        if b == 0:            return 1, 0, a        else:            x, y, r = ex_gcd(b, a % b)             x, y = y, (x - (a // b) * y)            return x, y, r
// c++int ex_gcd(int a, int b, int &x, int &y) {    if(b == 0) {         x = 1;        y = 0;        return a;    }    int r = ex_gcd(b, a % b, x, y);    int t = y;    y = x - (a / b) * y;    x = t;    return r;}

但是我们注意到,由于裴蜀定理,我们求解的丢番图方程中,等号右边的常数必须是 k * gcd(a, b)。所以我们的求解其实是:

c043fa04d460b6546c803e571cb066f9.png

所以通过扩展 GCD 算法求得的 x0y0 这组解,并不是我们要求的最终解。同样的,我们对其扩大 k 倍就是我们想要对结果:

dd246374406998dbaee161c390e1631a.png

小结

有了这些知识,你对那道“地狱难度”的头条面试题有没有更多的想法呢?这里有一道 [LeetCode-365] 水壶问题 你可以尝试一下,做完之后想必会对扩展 GCD 算法有更深的理解。

至于头条面试题,我将在下一篇文继续讲述并代码实现此题的解法。

1ce4ec1909cc8d0f9247f2c75fdbff5d.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值