扩展欧几里得算法

费马定理Fermat’s theorem)使我们能够通过二进制指数法(binary exponentiation)在O(logn)O(\log n)的操作内计算模数除法的乘法逆元(modular multiplicative inverses),但这只适用于对质数取模。它的一个推广是欧拉定理(Euler’s theorem),此定理表明,如果 m 和 a 互质(即他们之间除了1,没有其他公约数),那么

aϕ(m)1(modm)a^{\phi(m)} ≡ 1(\mod m)

其中,ϕ(m)\phi(m)欧拉函数,定义为小于mm的正整数xxmm互质的个数。当mm是一个质数时,所有小于mmm1m-1个正整数与mm都是互质的,所以ϕ(m)=m1\phi(m)=m-1,这就得出了费马定理。

这允许我们通过ϕ(m)\phi (m)来计算aa的模除乘法逆元,记作aϕ(m)1a^{\phi (m)−1},但反过来,计算ϕ(m)\phi (m)并不那么快速:通常你需要获得mm因数分解才能做到这一点。有一种更通用的方法,它是通过修改欧几里得算法Euclidean algorthm)实现的。

算法

扩展欧几里得算法(Extended Euclidean algorithm),除了用来寻找最大公约数g=gcd(a,b)g = \gcd (a,b),也可以用来寻找整数xxyy满足:

ax+by=ga \cdot x + b \cdot y = g

这可以通过将bb替换为mmgg替换为11来解决了寻找模运算逆元的问题:

a1a+km=1a^{−1}⋅a+k⋅m=1

如果aamm不是互质的,那么就没有解,因为aamm的任何整数组合都不能得到一个不是他们最大公约数的倍数的数。

算法同样是递归的:它先计算出gcd(b,amodb)\gcd (b,a \mod b)的系数xx'yy',然后恢复初始数字对的解。如果我们有一对(b,amodb)(b,a \mod b)的解(x,y)(x',y')

bx+(amodb)y=gb⋅x′+(a \mod b)⋅y′=g

那么,为了得到初始输入的解,我们可以将表达式(amodb)(a \mod b)重写为(abab)(a−⌊\frac{b}{a}⌋⋅b)并将它代入前述方程:

bx+(abab)y=gb⋅x′+(a−⌊\frac{b}{a}⌋⋅b)⋅y′=g

现在我们按aabb进行分组,重新排列表达式可以得到:

ayx+b(xbay)y=ga \cdot \underbrace{y'}_x + b \cdot \underbrace{(x' - ⌊\frac{b}{a}⌋ \cdot y')}_y = g

将上式与初始表达式相比较,可以得出aabb的系数即为初始的xxyy

实现

我们将算法实现为递归函数。由于它的输出不是一个而是三个整数,我们通过引用将系数传递给它:

int gcd(int a, int b, int &x, int &y) {
if (a == 0) {
x = 0;
y = 1;
return b;
}
int x1, y1;
int d = gcd(b % a, a, x1, y1);
x = y1 - (b / a) * x1;
y = x1;
return d;
}

为了计算模运算的乘法逆元,我们只需传递aamm,然后返回算法找到的xx系数。由于我们传递了两个正数,其中一个系数将是正的,另一个系数将是负的(具体哪一个取决于迭代次数是奇数还是偶数),所以我们需要检查xx是否为负,并加上mm以获得正确的余数。

int inverse(int a) {
int x, y;
gcd(a, M, x, y);
if (x < 0)
x += M;
return x;
}

这段代码的执行时间约为160ns,比用二进制指数法求逆元快10ns。为了进一步优化它,我们同样可以用迭代方式实现,这样的执行时间为135ns:

int inverse(int a) {
int b = M, x = 1, y = 0;
while (a != 1) {
y -= b / a * x;
b %= a;
swap(a, b);
swap(x, y);
}
return x < 0 ? x + M : x;
}

需要注意的是,和二进制指数法不同,执行时间取决于aa的值。例如,对于特定的mm(109+7)(10^9+7),性能表现最差的输入值为564400443564400443,这种情况下,算法执行37次迭代,花费250ns。

练习:可以尝试将相同的技术应用于二进制GCD(不过,除非你在优化方面比我强,否则这不会获得性能上的提升)。