欧几里得算法(辗转相除法)

邮差的信 提交于 2020-03-12 21:32:31

<!-- Euclid算法 -->

欧几里得算法即“辗转相除法”,用于求两个正整数的最大公约数(gcd)。

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

这是递归形式的代码,递归层数为4.785lgN + 1.6723,N是max(a, b),可见它递归不了多少层,不需要担心栈溢出的问题。

更精检的形式如下:

int gcd(int a, int b)
{
    return (b? gcd(b, a%b): a);
}

我们使用这段代码的时候,需要保证a和b是非负整数,a、b同为0的情况要预先处理掉。事实上我们不需要管a和b的大小关系,代码在递归中,始终将大数放在前,我们即使将小数放在前也就是多一层递归而已。

另外还有一种仅通过位运算就可以达到目的的gcd:

int gcd(int a, int b)
{
    int t = 1, c, d;
    while(a != b)
    {
        if(a < b) swap(a, b);
        if(! (a & 1)) { a >>= 1; c = 1; } else c = 0;
        if(! (b & 1)) { b >>= 1; d = 1; } else d = 0;
        if(c && d) t <<= 1;
        else if(! c && ! d) a -= b;
    }
    return t * a;
}

代码的思想是: 如果a和b均为偶数,则将公因子2提出来累加到因数t上,最终作为结果的系数一并返回; 如果a和b一个为偶,一个为奇,那么不可能同时含有公因子2,将为偶的数剔除2这个因子; 如果a和b都为奇数,则使用“更相减损术”计算其gcd。

同时注意代码始终保证a大于等于b,否则交换位置。其可行性在于gcd(a, b) = gcd(b, a)。 代码尽管是循环形式,但是却利用到了递推关系,如上述三句话用公式表示如下: a、b为偶数:gcd(a, b) = 2gcd(a/2, b/2); a、b其中之一为偶数(不妨设a):gcd(a, b) = gcd(a/2, b); a、b均为奇数:gcd(a, b) = gcd(a-b, b); 这个关系是递推的,所以可以利用循环去迭代。 另外注意这次的终止条件是a == b,不是b == 0。

这个算法的复杂度可能高于使用“辗转相除”的欧几里得算法,但是运行时间上可能相差不大。 因为代码中使用的都是位运算(比如我们判定某数位奇数或偶数的时候用到的是(x & 1)这个条件,除以2乘以2用到的是>>=1和<<=1),速度极快,不是除法运算和取模运算比得了的。另外代码非常巧妙的用c和d两个标记记录当前a、b的奇偶性。

<!-- 扩展Euclid算法 -->

欧几里得算法很好理解,代码也特别好写。下面说的这种算法是欧几里得算法的扩展,代码一样简练,但是理解起来却特别费劲。

扩展欧几里得算法的用处之一,是用来解决“线性不定方程求整数根”问题(数论是解决整数问题的理论)。形如ax+by=c这样的方程叫做线性不定方程,也就是我们初高中学的直线方程。

我们当前的目的是求ax+by=c的一组解,再考虑其他的解能不能推出来。

设d = gcd(a, b),那么ax+by=d一定有解,数论基础里面有一条重要的结论:两个数a、b(在这里我们都将其当做正整数来讨论)的最大公约数,一定等于a与b的线性组合(ax+by)中最小的正整数。

现在假设我们得到了ax+by=d的一组解(x0,y0),如果d是c的约数,那么ax+by=c对应的一组解只要将x0、y0乘以相应的倍数就好。

反之如果d不是c的约数,那么ax+by=c一定无解。为什么呢?因为ax+by一定是d的倍数,而c不是d的倍数,等号条件不可能成立。

下面我们用拓展欧几里得算法去求ax+by=d的解,这是我们之前没做的工作。

void gcd(int a, int b, int& d, int& x, int& y)
{
    if(!b) { d = a; x = 1; y = 0; }
    else
    {
        gcd(b, a%b, d, y, x);    // ( *  * )
        y -= x * (a/b);
    }
}

注意我们在调用gcd(a, b, d, x, y)的时候,最终得出的d、x、y是ax+by=d的解。注意d、x、y传递的是引用,刚声明的变量,直接传进去就好,函数会改变它们的值。 现在解释下代码的含义:

很容易看出来这段代码其中的一部分来自于我们刚才讲过的普通欧几里得算法,最终的d即是gcd(a, b),它自从在递归底层被赋值以后就没再变过。

我们辗转相除去求得了d以后,将x赋值为1,y赋值为0,得到了a x 1 + b x 0 = d这个等式,注意这里b也是0,所以事实上y赋值为什么整数都可以。

但是最需要注意的是,此时的x和y并不是ax+by=d的解,为什么?因为这时候的a和b已经不是原先的a和b了,经过数层的递归,a和b的值已经改变了。接下来要做的就是在回溯阶段算出上一层的x和y值,如果算得出来,在递归结束时,我们就能得到原方程的x和y(只有递归的第一层的a和b是原方程ax+by=d的a和b)。

注意加星号的那一行,我们在向下层递归的时候,传递的值分别是:b、a%b(这是为了辗转相除算gcd),d(最终保存gcd)、y、x(注意不是x、y)。 当回溯到当前层时,x和y的值已经算出来了,它满足下一层递归中式子的关系,我们需要找当前层的x、y和下层x、y的关系。

看这段代码:gcd(b, a%b, d, y, x),下层的参数列表是(int a, int b, int& d, int& x, int& y)。

这说明对这一层,有如下关系: by + (a % b)x = d。下面一层保证它成立,想想最下面一层是a x 1 + b x 0 = d。

我们希望对这一层有ax + by = d,显然从上面那个式子得不出这个式子,我们需要改变一下x或y。

正确的做法是将y减去x(a/b),我们来验证一下: a x + b (y - x(a/b)) = a x + b (y - x(a - a % b) / b) = b y - (a % b) x = d

可见对本层的a、b,将y减去x(a/b),x不变,就会得到满足ax + by = d的x和y。

可能有人会好奇这个x(a / b)是怎么来的,这个我觉得我们不用纠结(或者数学特别厉害的人能想办法找到这个差值),这个差值是算法的发明者找出来的,我们记下以后能写出算法来就好,毕竟我们只是利用扩展欧几里得算法解决问题。

之后的问题:

我们费了一番功夫,得到了ax + by = d的一组(x, y),如果d是c的约数,那么满足ax + by = c的x、y值我们也拿到了。

现在我们来找通解,将得到的这一组x和y记作x0、y0。

另一组x、y如果也满足ax + by = c,那么会有等式 a x + b y = a x0 + b y0。 进而有 a(x - x0) = b (y0 - y)。 我们将a、b约去d后,a'(x - x0) = b'(y0 - y),这个式子里由于a'和b'互素,式子要想成立,必须使x - x0是b'倍数,y - y0是a'倍数。 即: x = k b' + x0, y = k a' + y0。 注意我们取解的时候,这个k一旦定了,新的x和y要同时算出来,一个k值对应一组解。

到这里,我们能求出ax+by = c的通解了。

<!-- 更多变元 -->

现在扩展一下问题,我们来求ax + by + cz = d这个方程是否有解。

利用gcd(a, b, c) = gcd(gcd(a, b), c)就可以求得a、b、c的最大公约数,如果它也是d的约数,那么就有解,否则无解。

这么看来还可以推广一下结论:n个整数(这里指正整数)的gcd,是这n个整数线性组合的最小正整数。

<!-- 一些废话 -->

数论的内容晦涩难懂,机关重重,解题方式也是变化多端,对数学思维要求很高。也是我自己又爱又恨的一部分内容(爱是觉得很巧妙,恨是想不出来)。

我尽量把这些内容写得清楚明白,希望跟我一样的算法爱好者在入门的时候少走弯路,不被抹杀掉搞算法的兴趣和激情。

最后吐槽一下开源中国的编辑器,实在是用着难受。。。可能是我不怎么会用吧,各种符号不能使用,代码也莫名奇妙就给我注释掉了。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!