谈谈扩展欧几里得算法

扩展欧几里得算法

欧几里得算法是数论中用于求得两个整数 aa bb 的最大公约数 c=(a,b)c=(a,b) 的算法。我之前的文章中已经介绍了这个算法的正确性来源于 (a,b)=(akb),kZ(a,b)=(a-k b), k\in \Z 。而扩展欧几里得算法是一个可以得到

sa+tb=(a,b) sa+t b=(a,b)

中的系数 ss tt 的算法。

我第一次看到的这个算法实现来自于一位精通算法竞赛的同学他给出的实现类似于

1int exgcd(int a,int b,int &x,int &y)
2{
3    if(b==0)
4    {
5        x = 1; y = 0; return a;
6    }
7    int r = exgcd(b, a % b, x, y);
8    int t = y;
9    y = x - (a / b) * y;
10    x = t;
11    return r;
12}

这个实现把参数xy当作传递结果的寄存器有比较浓厚的命令式风格。

用比较易于理解的函数式风格实现如下

1let rec ext_gcd a b = 
2  if b = 0 then (1, 0, a)
3  else 
4    let quo = a / b
5    let r = a % b
6    let (s, t, gcd) = ext_gcd b r
7    (t, s - quo * t, gcd)

实际上上面的两种实现都是基于一个手工的代换过程。以下述欧几里得算法过程为例

20÷7=2...67÷6=1...16÷1=6...0 20 \div 7 = 2 ... 6 \\ 7 \div 6 = 1 ... 1 \\ 6 \div 1 = 6 ... 0

将每个除法式都变成无除法的加减法式

202×7=671×6=161×6=0 \begin{aligned} 20 - 2 \times 7 = 6 \\ 7 - 1 \times 6 = 1 \\ 6 - 1 \times 6 = 0 \end{aligned}

考虑倒数第二个式子

71×6=1 7 - 1 \times \textcolor{red}{6} = 1

把这个式子里的 6\textcolor{red}{6} 202×7=620 - 2 \times 7 = \textcolor{red}{6} 代换我们得到

71×(202×7)=11×20+3×7=1 7 - 1 \times (20 - 2 \times 7) = 1 \\ -1 \times 20 + 3 \times 7 = 1

更一般地说两个式子为

s1a1+t1b1=c1s2b1+t2c1=c2 \begin{aligned} s_1 a_1 + t_1 b_1 = c_1 \\ s_2 b_1 + t_2 c_1 = c_2 \end{aligned}

那么把他们进行合并代换就会变为

(s1t2)a1+(s2+t1t2)b1=c2 (s_1 t_2) a_1 + (s_2 + t_1 t_2) b_1 = c_2

显然地可以把这个过程抽象为一个对三元组的运算这里用*表示

(s1,t1,c1)(s2,t2,c2)=(s1t2,s2+t1t2,c2) (s_1, t_1, c_1) * (s_2,t_2,c_2)=(s_1t_2, s_2 + t_1 t_2, c_2)

这时再看那个函数式程序我们会清晰很多

ext_gcd b r求出的是sb+tr=gcds b + tr = gcd的三元组(s,t,gcd)(s, t, gcd)而此三元组与本次除法定义的1×a+(quo)×b=r1 \times a + (-quo) \times b = r之三元组(1,quo,r)(1, -quo, r)正好可以进行*运算

(1,quo,r)(s,t,gcd)=(t,s(quo)t,gcd) (1, -quo, r) * (s, t, gcd) = (t, s-(quo)t, gcd)

这正是代码中的返回值(t, s - quo * t, gcd).

我们可以给出直接使用这个运算的程序

1let F x1 x2 = 
2  let s1, t1, c1 = x1
3  let s2, t2, c2 = x2 
4  (s1 * t2, s2 + t1 * t2, c2)
5
6let rec ext_gcd2 a b =
7  if b = 0 then (1, 0, a)
8  else 
9    let quo = a / b
10    let r = a % b
11    F (1, -quo, r) (ext_gcd2 b r)

从递归到循环

Wand 的模式

40多年以前MITCHELL WAND发表了一篇论文叫做 Continuation-Based Program Transformation Strategies他总结了把递归程序变成循环程序的很多通用方法。

当然我们这里的循环递归的真正意义还需要进一步规范比如说

1(define (factor n)
2  (let loop ([n n] [cont (lambda (x) x)])
3    (if (= n 0)
4        (cont 1)
5        (loop (- n 1) (lambda (x) (cont (* n x)))))))

这种循环不是此处研究的循环我们所说的循环必须是使用固定空间空间复杂度为O(1)O(1)的东西。

通过研究类似于factor这种函数的两种写法

1(define (factor n)
2  (if (= n 0)
3      1
4      (* n (factor (- n 1)))))
1(define (factor n)
2  (let loop ([n n][acc 1])
3    (if (= n 0)
4        acc
5        (loop (- n 1) (* acc now)))))

WAND总结出了第一个模式

要求是 bb 这个函数是 AAAA \rightarrow A \rightarrow A 的运算且具有结合性其中 lbl_b 是右单位元意思是说b(x,lb)=xb(x, l_b)=x

我们是否可以用这个模式对上面的程序进行变换呢看似是可以的因为这里的 p(x)p(x)a(x)a(x)bbFFc(x)c(x)d(x)d(x) 都可以明确地给出。

然而这里有一个问题上面定义的运算 * 到底是不是结合性的呢

结合性

一个运算的结合性本质上描述了这个运算在自组合的时候能不能改变其运算顺序。考虑一个运算ff它具有结合性就是说

f(a,f(b,c))=f(f(a,b),c) f(a,f(b, c))=f(f(a, b), c)

然而我们的运算似乎没有结合性

这是怎么回事找个具体例子研究一下

(54,12)=11×55+(3)×16=7,(1,3,7)1×16+(2)×7=2,(1,2,2)1×7+(3)×2=1,(1,3,1)1×2+(2)×1=0,(1,2,0) (54, 12) = 1 \\ 1 \times 55 + (-3) \times 16 = 7, (1, -3, 7) \\ 1 \times 16 + (-2) \times 7 = 2, (1, -2, 2) \\ 1 \times 7 + (-3) \times 2 = 1, (1, -3, 1) \\ 1 \times 2 + (-2) \times 1 = 0, (1, -2, 0) \\

以前三个为例如果正着做

这会得到令人非常吃惊的结果因为

6×5520×16=10 6 \times 55 - 20 \times 16 = 10

而我们的组合运算理论上应该得到

6×5520×16=1 6 \times 55 - 20 \times 16 = 1

问题出在哪里呢

考虑 (1,3,7)(1,2,2)=(2,7,2)(1, -3, 7) * (1, -2, 2)=(-2, 7, 2)等号右侧三元组的意义是

2×55+7×16=2 -2 \times 55 + 7 \times 16 = 2

这个式子不能和

1×7+(3)×2=1 1 \times 7 + (-3) \times 2 = 1

\*\* 进行运算。因为能与 2×55+7×16=2-2 \times 55 + 7 \times 16 = 2 进行 * 运算的式子一定形如

a×16+b×2=c a \times 16 + b \times 2 = c

可见这已经不仅是没有结合性的问题。对于我们定义的 \*\* 运算来说 (a\*b)\*c(a\*b)\*c 根本没有意义。

这样一来单纯地用Wang的方法不可能把这个递归变换为循环。

如何循环实现

再次观察上面的式子

1×55+(3)×16=7,(1,3,7)1×16+(2)×7=2,(1,2,2)1×7+(3)×2=1,(1,3,1) 1 \times 55 + (-3) \times 16 = \textcolor{red}{7}, (1, -3, 7) \\ 1 \times 16 + (-2) \times \textcolor{red}{7} = \textcolor{blue}{2}, (1, -2, 2) \\ 1 \times \textcolor{red}{7} + (-3) \times \textcolor{blue}{2} = 1, (1, -3, 1) \\

我们好像发现了一种新的模式

sn2a1+tn2b1=cn2sn1a1+tn1b1=cn1scn2+tcn1=c3 s_{n-2} a_1 + t_{n-2} b_1 = c_{n-2} \\ s_{n-1} a_1 + t_{n-1} b_1 = c_{n-1} \\ s c_{n-2} + t c_{n-1} = c_3

如果能够建立上面的关系那么sns_ntnt_n就都是可以计算的

s(sn2a1+tn2b1)+t(sn1a1+tn1b1)=c3sn=ssn2+tsn1tn=stn2+ttn1 s (s_{n-2} a_1 + t_{n-2} b_1) + t (s_{n-1} a_1 + t_{n-1} b_1) = c_3 \\ s_n = ss_{n-2}+ ts_{n-1} \\ t_n = st_{n-2}+tt_{n-1}

令人头痛的是开头两个式子的建立。实际上读者如果认真研究过递归版的算法最后一个式子的建立用了一个有趣的技巧。我们这里也可以运用相同的技巧但这技巧为何成立就留给读者自行研究吧

s0=1,t0=0s1=0,t1=1 s_0 = 1, t_0 = 0 \\ s_1 = 0, t_1 = 1

实现如下

1let rec ext_gcd_loop a b =
2  if b = 0 then (1, 0, a)
3  else 
4    let rec loop a b s_n1 t_n1 s_n2 t_n2 = 
5      let quo = a / b 
6      let r = a % b
7      let s = 1
8      let t = -quo
9      if r = 0 then (s_n1, t_n1, b)
10      else 
11        loop b r (s * s_n2 + t * s_n1) (s * t_n2 + t * t_n1) s_n1 t_n1
12    loop a b 0 1 1 0