浅谈扩展更相减损法在解二元一次不定方程领域的应用

· · 算法·理论

算法引入

在学习数论时,我们会学习一种算法,叫做扩展欧几里得算法,可以用于解二元一次不定方程,时间复杂度为 \operatorname O(\log(\max(a,b))

不过,我们还可以找到另一种算法来解二元一次不定方程。
是谁呢? 更相减损法(Alternating Subtraction Algorithm,ASA)

更相减损法

本体

式子如下(想看证明的看这):

\gcd(b,a) & (a<b) \\ \gcd(b,a-b) & (a\ge b \bigwedge b\ne0) \\ a & (b=0) \end{cases}
举个例子 求 114514 的最大公因数: a b
114 514
514 114
114 514-114=400
400 114
114 400-114=286
286 114
114 286-114=172
172 114
114 172-114=58
58 114-58=56
56 58-56=2
\cdots \cdots
4 2
2 2
2 0

所以,114514 的最大公因数是 2

到省略号部分时,我们可以发现,当 ab 差距过大时,该算法效率会大幅降低。如果构造 \{n,1\} 这样的数据,复杂度会退化回 \operatorname O(n)

所以, \text{\color{red}需要优化}

优化·式子

显然, 当 a , b 都是 2 的倍数时

\gcd(a,b)=2 \times \gcd(a \div 2,b \div 2 )

否则,当 a2 的倍数时

\gcd(a,b)=\gcd(a \div 2,b)

否则,当 b2 的倍数时

\gcd(a,b)=\gcd(a,b \div 2)

我们,就可以就此,利用移位运算(不会见此)除以 2 优化。

则最终式子为(其中 x \mid y 表示 yx 的倍数,x \nmid y 表示 y 不是 x 的倍数):

(1)a & (b=0) \\ (2)2 \times \gcd(a \div 2,b \div 2) & (2\mid a \bigwedge 2\mid b \bigwedge b\ne0) \\ (3)\gcd(a \div 2,b) & (2\mid a \bigwedge 2\nmid b \bigwedge b\ne0) \\ (4)\gcd(a,b \div 2) & (2\nmid a \bigwedge 2\mid b \bigwedge b\ne0) \\ (5)\gcd(b,a) & (a<b \bigwedge 2\nmid a \bigwedge 2\nmid b \bigwedge b\ne0) \\ (6)\gcd(b,a-b) & (a\ge b \bigwedge 2\nmid a \bigwedge 2\nmid b \bigwedge b\ne0) \\ \end{cases}

优化·时间复杂度

先说结论:稳定为 \operatorname O(\log(\max(a,b)))
为什么呢?

证明如下:

命题 1

内容:当递归不在 (2) 式时,以后也不可能进入 (2)式。

证明:对于 (1)(5) 式,显然。
对于 (3) 式,已经 2 \nmid b ,在运行完之后依旧如此,不会进入 (2) 式。
对于 (4) 式,同上。
对于 (6) 式,因为 2 \nmid a2 \nmid b

a \equiv 1 \pmod 2 b \equiv 1 \pmod 2

所以

a-b \equiv 0 \pmod 2

2 \mid (a-b)
因为 2 \nmid b , 2 \mid (a-b)
所以,在 (6) 式之后的下一个式子,一定是 (4)(5) 式。

综上,命题成立。

命题 2

内容: (1) 式只能出现一次。

证明:易证,略。

命题 3

内容:(5)(6) 式不能连续 4 次出现。

证明:对于 (5) 式,显然。
对于 (6) 式,见命题 1(6) ,成立。
对于 (5)(6) 式一起,由命题 1(6) ,(6)式以后为 (4)(5) 式。
在此时 (5) 式以后,只能为 (4)$式。

综上,命题成立。

证明

现在开始分析。

(1)(5)(6) 式不会连续出现,且附着在 (3)(4) 式后,可以忽略。
(2)(3)(4) 式最多运行 \log 级别的次数。

所以,时间复杂度为稳定的 \operatorname O(\log(\max(a,b)))

Q.E.D.

优化·代码部分

现在,我们根据这个函数,写下它的代码。

int gcdt(int a,int b)
{
    if(b==0)//(1)式 
    {
        return a;
    }
    while(!(a&1))//(3)式 
    {
        a>>=1;
    }
    while(!(b&1))//(4)式 
    {
        b>>=1;
    }
    /*
    注意,根据证明,(2)式不会出现,(3)(4)式
    显然不会同时出现 
    */
    if(a<b)//(5)式 
    {
        return gcdt(a,b-a);
    }
    return gcdt(b,a-b);//(6)式 
}

int gcd2(int a,int b)//真正的函数入口
{
    int ans=1;
    while(!(a&1||b&1))//先行处理(2)式 
    {
        ans<<=1;
        a>>=1;
        b>>=1;
    }
    return ans*gcdt(a,b);//不要忘了乘回去 
}

顺带一提,上文所述优化也被称作Stein算法,可用于计算整数的最大公因数。

真的开始:扩展·更相减损法

推导

我们所要解的,是以下的方程:

ax+by =\gcd(a,b)

我们所要利用的,式刚刚推出来的式子:

(1)a & (b=0) \\ (2)2 \times \gcd(a \div 2,b \div 2) & (2\mid a \bigwedge 2\mid b \bigwedge b\ne0) \\ (3)\gcd(a \div 2,b) & (2\mid a \bigwedge 2\nmid b \bigwedge b\ne0) \\ (4)\gcd(a,b \div 2) & (2\nmid a \bigwedge 2\mid b \bigwedge b\ne0) \\ (5)\gcd(b,a) & (a<b \bigwedge 2\nmid a \bigwedge 2\nmid b \bigwedge b\ne0) \\ (6)\gcd(b,a-b) & (a\ge b \bigwedge 2\nmid a \bigwedge 2\nmid b \bigwedge b\ne0) \\ \end{cases}

为了方便,设 a',b' 为进入式子后的 a,b 值。

x',y' 满足

a'x'+b'y'=gcd(a',b')

下面,开始解!方!程!

(1) 式

ax+by &= \gcd(a,b)\\ &=a\\ &=a \times 1 + b \times 0 \end{aligned}

在其中, x=1,y=0 显然是一组解。

(2) 式

ax+by &= 2 \times\gcd(a \div 2,b \div 2)\\ \end{aligned}

a'=a \div 2,b'=b \div 2
由于

\gcd(a',b')&=a'x'+b'y'\\ &=\frac{ax'}{2} + \frac{by'}{2}\\ &=a \times \frac{x'}{2}+ b \times \frac{y'}{2} \end{aligned}

所以

ax+by&=2\times\gcd(a',b')\\ &=2(a\times\frac{x'}{2}+b\times\frac{y'}{2})\\ &=ax'+by' \end{aligned}

所以,x=x',y=y' 是一组特解。

(3) 式

ax+by &=\gcd(a \div 2,b)\\ \end{aligned}

a'=a \div 2,b'=b
由于

\gcd(a',b')&=a'x'+b'y'\\ &=\frac{ax'}{2} + by'\\ &=a \times \frac{x'}{2}+ by' \end{aligned}

所以

ax+by&=\gcd(a',b')\\ &=a\times\frac{x'}{2}+by' \end{aligned}

所以,x=\frac{x'}{2},y=y' 是一组特解。

(4) 式

ax+by &=\gcd(a,b \div 2)\\ \end{aligned}

a'=a,b'=b \div 2
由于

\gcd(a',b')&=a'x'+b'y'\\ &=ax' + \frac{by'}{2}\\ &=ax'+b \times \frac{y'}{2} \end{aligned}

所以

ax+by&=\gcd(a',b')\\ &=ax'+b \times \frac{y'}{2} \end{aligned}

所以,x=x',y=\frac{y'}{2} 是一组特解。

(5) 式

ax+by &=\gcd(b,a)\\ \end{aligned}

a'=b,b'=a
由于

\gcd(a',b')&=a'x'+b'y'\\ &=bx'+ay'\\ &=ay'+bx' \end{aligned}

所以

ax+by&=\gcd(a',b')\\ &=ay'+bx' \end{aligned}

所以,x=y',y=x' 是一组特解。

(6) 式

ax+by &=\gcd(b,a-b)\\ \end{aligned}

a'=b,b'=a-b
由于

\gcd(a',b')&=a'x'+b'y'\\ &=bx'+(a-b)y'\\ &=ay'+b(x'-y') \end{aligned}

所以

ax+by&=\gcd(a',b')\\ &=ay'+b(x'-y') \end{aligned}

所以,x=y',y=x'-y' 是一组特解。

漏洞与解决

刚刚的推导看似无懈可击,但对于 (3)(4) 式,有致命的漏洞。

x,y都必须要是整数!

而在 (3)(4) 式中,出现了形如 \frac{x'}{2} 这样的式子。

例如,在 (3) 式中,当 2 \mid x' 时, \frac{x'}{2} 是整数,无事发生。

但是 2 \nmid x' 时,\frac{x'}{2} 不为整数,算法崩溃了
……吗?

在 (3) 式内,根据取值

2\mid a,2 \nmid b

设整数 r_1,r_2 满足

a(\frac{x'}{2}+\frac{r_1}{2})+b(y'+r_2)=\gcd(a,b)

于是,括号内两式为整数。

由上文

a\times\frac{x'}{2}+by'=\gcd(a,b)

上下两式作差得

a\times\frac{r_1}{2}+br_2=0 r_2=-\frac{a}{2b}\times r_1 r_2=-\frac{ \frac{a}{\gcd(a,2b)} }{\frac{2b}{\gcd(a,2b)}}\times r_1

此时右式中的分式部分已经是最简分数,为使 r_1 , r_2 为整数,令

&=\frac{b}{\gcd(a\div 2,b)}\times n \end{aligned} r_2&=-\frac{a}{\gcd(a,2b)}\times n \\ &=-\frac{a \div 2}{\gcd(a\div 2,b)}\times n \end{aligned}

(其中, n 是一个整数)

我们想要 \frac{x'}{2}+\frac{r_1}{2} 为整数,所以

r_1+x'\equiv 0 \pmod 2 \frac{b}{\gcd(a\div 2,b)}\times n\equiv -x' \pmod 2

因为 2\nmid x',2\nmid b,2\nmid\gcd(a\div 2,b)
所以

\frac{b}{\gcd(a\div 2,b)}\equiv 1 \pmod 2 -x'\equiv 1 \pmod 2

所以

n\equiv1\pmod 2

为了 卡常 方便,令

n=\gcd(a\div 2,b)

2\nmid b

\gcd(a\div 2,b)\equiv 1\pmod2

满足上式。

所以

r_1=b r_2=-\frac{a}{2}

那么 (4) 式又如何呢?

因为推导过程和 (3) 式相同,所以过程省略,结果如下:

r_1=-\frac{b}{2} r_2=a

那么,针对 (3)(4) 式的“女娲补天”完成。

代码(例)

我们注意到,(5) 式后面一定递归 (6) 式。
所以,我们可以通过此种方式减少递归,一定程度上加快算法。

我们以扩欧模板题之一 P1082为例

依照扩展更相减损法,在 C++14(GCC9) 情况下,代码如下:

#include <cstdio>

using namespace std;
#define int long long
int ans1,ans2;

void exgcd_asa(int a,int b)
{
    if(b==0)
    {
        ans1=1,ans2=0;return;
    }
    if(!(a&1))
    {
        exgcd_asa(a>>=1,b);
        if(ans1&1)
        {
            ans1+=b;
            ans2-=a;
        }
        ans1>>=1;return;
    }
    if(!(b&1))
    {
        exgcd_asa(a,b>>=1);
        if(ans2&1)
        {
            ans1-=b;
            ans2+=a;
        }
        ans2>>=1;return;
    }
    if(a<b)
    {
        exgcd_asa(a,b-a);
        int temp=ans2;
        ans1=ans1-temp;return;
    }
    exgcd_asa(b,a-b);
    int temp=ans1;
    ans1=ans2;
    ans2=temp-ans1;
    return;
}
void exasa(int a,int b)
{
    while(!(a&1||b&1))
    {
        a>>=1,b>>=1;
    }
    exgcd_asa(a,b);
}

signed main()
{
    int a,b;
    scanf("%lld%lld",&a,&b);
    exasa(a,b);
    printf("%lld\n",(ans1%b+b)%b);
    return 0;
}

后记

\text{\color{black}谢谢观看。}