❖ 扩展欧几里得算法 欧几里得算法 是数论中用于求得两个整数 a a a 和 b b b 的最大公约数 c = ( a , b ) c=(a,b) c = ( a , b ) 的算法。我之前的文章中已经介绍了这个算法的正确性来源于 ( a , b ) = ( a − k b ) , k ∈ Z (a,b)=(a-k b), k\in \Z ( a , b ) = ( a − kb ) , k ∈ Z 。而 “扩展欧几里得算法 ”是一个可以得到
s a + t b = ( a , b )
sa+t b=(a,b)
s a + t b = ( a , b ) 中的系数 s s s 和 t t t 的算法。
我第一次看到的这个算法实现 , 来自于一位精通算法竞赛的同学 , 他给出的实现类似于
1 int 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 }
这个实现把参数 x
和 y
当作传递结果的寄存器 , 有比较浓厚的命令式风格。
用比较易于理解的函数式风格实现如下 :
1 let 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...6 7 ÷ 6 = 1...1 6 ÷ 1 = 6...0
20 \div 7 = 2 ... 6 \\
7 \div 6 = 1 ... 1 \\
6 \div 1 = 6 ... 0
20 ÷ 7 = 2...6 7 ÷ 6 = 1...1 6 ÷ 1 = 6...0 将每个除法式都变成无除法的加减法式 :
20 − 2 × 7 = 6 7 − 1 × 6 = 1 6 − 1 × 6 = 0
\begin{aligned}
20 - 2 \times 7 = 6 \\
7 - 1 \times 6 = 1 \\
6 - 1 \times 6 = 0
\end{aligned}
20 − 2 × 7 = 6 7 − 1 × 6 = 1 6 − 1 × 6 = 0 考虑倒数第二个式子
7 − 1 × 6 = 1
7 - 1 \times \textcolor{red}{6} = 1
7 − 1 × 6 = 1 把这个式子里的 6 \textcolor{red}{6} 6 用 20 − 2 × 7 = 6 20 - 2 \times 7 = \textcolor{red}{6} 20 − 2 × 7 = 6 代换 , 我们得到 :
7 − 1 × ( 20 − 2 × 7 ) = 1 − 1 × 20 + 3 × 7 = 1
7 - 1 \times (20 - 2 \times 7) = 1 \\
-1 \times 20 + 3 \times 7 = 1
7 − 1 × ( 20 − 2 × 7 ) = 1 − 1 × 20 + 3 × 7 = 1 更一般地说 , 两个式子为
s 1 a 1 + t 1 b 1 = c 1 s 2 b 1 + t 2 c 1 = c 2
\begin{aligned}
s_1 a_1 + t_1 b_1 = c_1 \\
s_2 b_1 + t_2 c_1 = c_2
\end{aligned}
s 1 a 1 + t 1 b 1 = c 1 s 2 b 1 + t 2 c 1 = c 2 那么 , 把他们进行合并代换 , 就会变为 :
( s 1 t 2 ) a 1 + ( s 2 + t 1 t 2 ) b 1 = c 2
(s_1 t_2) a_1 + (s_2 + t_1 t_2) b_1 = c_2
( s 1 t 2 ) a 1 + ( s 2 + t 1 t 2 ) b 1 = c 2 显然地 , 可以把这个过程抽象为一个对三元组的运算 , 这里用 ∗ * ∗ 表示 , 即 :
( s 1 , t 1 , c 1 ) ∗ ( s 2 , t 2 , c 2 ) = ( s 1 t 2 , s 2 + t 1 t 2 , c 2 )
(s_1, t_1, c_1) * (s_2,t_2,c_2)=(s_1t_2, s_2 + t_1 t_2, c_2)
( s 1 , t 1 , c 1 ) ∗ ( s 2 , t 2 , c 2 ) = ( s 1 t 2 , s 2 + t 1 t 2 , c 2 ) 这时再看那个函数式程序 , 我们会清晰很多 :
ext_gcd b r
求出的是 s b + t r = g c d s b + tr = gcd s b + t r = g c d 的三元组 ( s , t , g c d ) (s, t, gcd) ( s , t , g c d ) , 而此三元组与本次除法定义的 1 × a + ( − q u o ) × b = r 1 \times a + (-quo) \times b = r 1 × a + ( − q u o ) × b = r 之三元组 ( 1 , − q u o , r ) (1, -quo, r) ( 1 , − q u o , r ) 正好可以进行 ∗ * ∗ 运算 , 而
( 1 , − q u o , r ) ∗ ( s , t , g c d ) = ( t , s − ( q u o ) t , g c d )
(1, -quo, r) * (s, t, gcd) = (t, s-(quo)t, gcd)
( 1 , − q u o , r ) ∗ ( s , t , g c d ) = ( t , s − ( q u o ) t , g c d ) 这正是代码中的返回值 (t, s - quo * t, gcd)
.
我们可以给出直接使用这个运算的程序 :
1 let F x1 x2 =
2 let s1 , t1 , c1 = x1
3 let s2 , t2 , c2 = x2
4 ( s1 * t2 , s2 + t1 * t2 , c2 )
5
6 let 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) 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总结出了第一个模式 :
要求是 b b b 这个函数是 A → A → A A \rightarrow A \rightarrow A A → A → A 的运算 , 且具有结合性 , 其中 l b l_b l b 是右单位元 ( 意思是说 b ( x , l b ) = x b(x, l_b)=x b ( x , l b ) = x ) 。
我们是否可以用这个模式对上面的程序进行变换呢 ? 看似是可以的 , 因为这里的 p ( x ) p(x) p ( x ) , a ( x ) a(x) a ( x ) , b b b , F F F , c ( x ) c(x) c ( x ) , d ( x ) d(x) d ( x ) 都可以明确地给出。
然而 , 这里有一个问题 : 上面定义的运算 ∗ * ∗ 到底是不是结合性的呢 ?
❖ 结合性 ? 一个运算的 “结合性 ”, 本质上描述了这个运算在 “自组合 ”的时候能不能改变其运算顺序。考虑一个运算 f f f , 它具有结合性就是说
f ( a , f ( b , c ) ) = f ( f ( a , b ) , c )
f(a,f(b, c))=f(f(a, b), c)
f ( a , f ( b , c )) = f ( f ( a , b ) , c ) 然而 , 我们的运算似乎没有结合性 :
这是怎么回事 ? 找个具体例子研究一下 :
( 54 , 12 ) = 1 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 × 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) \\
( 54 , 12 ) = 1 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 × 2 + ( − 2 ) × 1 = 0 , ( 1 , − 2 , 0 ) 以前三个为例 , 如果正着做 :
这会得到令人非常吃惊的结果 , 因为
6 × 55 − 20 × 16 = 10
6 \times 55 - 20 \times 16 = 10
6 × 55 − 20 × 16 = 10 而我们的组合运算理论上应该得到
6 × 55 − 20 × 16 = 1
6 \times 55 - 20 \times 16 = 1
6 × 55 − 20 × 16 = 1 问题出在哪里呢 ?
考虑 ( 1 , − 3 , 7 ) ∗ ( 1 , − 2 , 2 ) = ( − 2 , 7 , 2 ) (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
− 2 × 55 + 7 × 16 = 2 这个式子不能和
1 × 7 + ( − 3 ) × 2 = 1
1 \times 7 + (-3) \times 2 = 1
1 × 7 + ( − 3 ) × 2 = 1 用 \* \* \* 进行运算。因为能与 − 2 × 55 + 7 × 16 = 2 -2 \times 55 + 7 \times 16 = 2 − 2 × 55 + 7 × 16 = 2 进行 ∗ * ∗ 运算的式子一定形如
a × 16 + b × 2 = c
a \times 16 + b \times 2 = c
a × 16 + b × 2 = c 可见 , 这已经不仅是没有结合性的问题。对于我们定义的 \* \* \* 运算来说 ( a \* b ) \* 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) \\
1 × 55 + ( − 3 ) × 16 = 7 , ( 1 , − 3 , 7 ) 1 × 16 + ( − 2 ) × 7 = 2 , ( 1 , − 2 , 2 ) 1 × 7 + ( − 3 ) × 2 = 1 , ( 1 , − 3 , 1 ) 我们好像发现了一种新的模式 :
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
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
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 如果能够建立上面的关系 , 那么 s n s_n s n 和 t n t_n t n 就都是可以计算的 :
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 = s s n − 2 + t s n − 1 t n = s t n − 2 + t t n − 1
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}
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 = s s n − 2 + t s n − 1 t n = s t n − 2 + t t n − 1 令人头痛的是开头两个式子的建立。实际上读者如果认真研究过递归版的算法 , 最后一个式子的建立用了一个有趣的技巧。我们这里也可以运用相同的技巧 , 但这技巧为何成立就留给读者自行研究吧 :
s 0 = 1 , t 0 = 0 s 1 = 0 , t 1 = 1
s_0 = 1, t_0 = 0 \\
s_1 = 0, t_1 = 1
s 0 = 1 , t 0 = 0 s 1 = 0 , t 1 = 1 实现如下 :
1 let 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