Euclidean Algorithm

August 09, 2020

나눗셈과 최대공약수

임의의 두 정수 a,ba, b에 대해, (a,b)(a, b)aabb의 최대공약수라고 하자. a=bq+ra = bq + r일 때, (a,b)=(b,r)(a, b) = (b, r)이다. 증명은 어렵지 않다. 임의의 정수 cc에 대해 c(a,b)c \mid (a, b)라면 cabq=rc \mid a - bq = r이므로 c(b,r)c \mid (b, r)이고, 반대 방향도 같은 방법으로 보일 수 있다.

유클리드 알고리즘

유클리드 알고리즘은 나눗셈과 최대공약수의 이러한 성질을 이용하여 임의의 두 정수 a,ba, b의 최대공약수를 빠르게 구하는 알고리즘이다. 다음과 같은 나눗셈 열을 생각하자.

a=bq1+r1b=r1q2+r2rn1=rnqn+rn+1rn+1=0\begin{align*} a & = b q_1 + r_1 \\ b & = r_1 q_2 + r_2 \\ & \vdots \\ r_{n - 1} & = r_n q_n + r_{n + 1} \\ r_{n + 1} & = 0 \end{align*}

마지막 나눗셈에서는 rn1r_{n - 1}rnr_n으로 나누어 떨어진다. 이는 유클리드 알고리즘의 기저 사례라고 할 수 있다. 임의의 0이 아닌 정수 mm에 대해 (m,0)=m(m, 0) = m이기 때문이다. 유클리드 알고리즘을 통해 (a,b)=rn(a, b) = r_n임을 알 수 있다. 나눗셈 열에서 rm=0r_m = 0이 등장하여 알고리즘이 유한한 수행을 통해 정지한다는 사실은, 수열 b>r1>r2>b > r_1 > r_2 > \cdots가 감소하는 음이 아닌 정수 수열을 이룸을 관찰함으로써 알 수 있다.

구현

유클리드 알고리즘을 다음과 같이 구현할 수 있다.

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

이는 꼬리 재귀 함수이다. 마음이 편안해진다.

일단 재귀 함수로 구현을 하였지만, 재귀 함수의 호출은 종종 많아져서는 메모리를 터뜨리게 된다. 알고리즘에 등장하는 나눗셈 열이 얼마나 길게 이어지는지 알아보기 위해 알고리즘을 분석하여 보자.

분석

입력이 aba \ge b으로 주어졌다고 가정하자. 피보나치 수열을 사용하여 kk의 증가에 따라 rkr_k가 지수적으로 감소한다는 사실을 증명할 것이다. 피보나치 수열은 점화식을 통해 주어지는 수열으로,

{f0=0,f1=1,fn=fn1+fn2(n2)\begin{cases} f_0 & = 0, \\ f_1 & = 1, \\ f_n & = f_{n - 1} + f_{n - 2} & (n \ge 2) \end{cases}

를 만족한다. b<fkb < f_k가 되는 정수 kk를 선택하고, 표기의 편의를 위해 r0=br_0 = b로 설정하자. r1<fk1r_1 < f_{k - 1}임을 보이고 싶지만, 이는 간단한 반례를 통해 반증될 수 있는 명제이다. (23,12)(23, 12)를 유클리드 알고리즘을 통해 계산하는 상황을 생각하면, 처음 설정하는 피보나치 수는 f7=13f_7 = 13이다. r1=118=f6r_1 = 11 \ge 8 = f_6이므로 r1<fk1r_1 < f_{k - 1}는 거짓이다.

그러나 이가 거짓일 때 관찰할 수 있는 사실이 하나 있다. 위의 예시에서 나눗셈을 한 번 더 진행하여 보자. r2=1<5=f5r_2 = 1 < 5 = f_5임을 알 수 있다. 사실은 r1<fk1r_1 < f_{k - 1}이 거짓인 경우, r2<fk2r_2 < f_{k - 2}는 항상 참이다. 임의의 mm에 대해, rm<fkr_m < f_k이고 rm+1fk1r_{m + 1} \ge f_{k - 1}을 가정하자. 다음을 관찰하자: rm+2rmrm+1<fkfk1=fk2.r_{m + 2} \le r_m - r_{m + 1} < f_k - f_{k - 1} = f_{k - 2}. 따라서 r0<fkr_0 < f_k인 정수 kk에 대해, 유클리드 알고리즘은 많아야 k+1k + 1번의 나눗셈을 수행할 것이다. fkO(φk),φ=1+52f_k \in \mathcal{O} ( \varphi^k ), \varphi = \frac{1 + \sqrt{5}}{2}이므로, 유클리드 알고리즘은 O(logr0)\mathcal{O}(\log r_0)의 시간복잡도를 갖는다.

확장된 유클리드 알고리즘

개요: 디오판토스 방정식 ax+by=(a,b)ax + by = (a, b)

임의의 두 정수 a,ba, b에 대해 ax+by=(a,b)ax + by = (a, b)를 만족하는 정수 x,yx, y가 존재한다. 정수 집합의 정렬성을 이용하여 증명할 수 있으나, 유클리드 알고리즘을 이용하여 증명하는 것이 좀 더 와닿는다고 생각한다. 우선 정수의 부분집합 S={ax+by:x,yZ}S = \{ax + by: x, y \in \mathbb{Z}\}를 생각하자. 분명하게 a=r1,b=r0Sa = r_{-1}, b = r_0 \in S이다. SS에 대해 다음 성질들을 관찰하자.

  1. SS는 덧셈에 대해 닫혀있다. SS의 임의의 두 원소 ax+by,au+bvax + by, au + bv에 대해

(ax+by)+(au+bv)=a(x+u)+b(y+v)S(ax + by) + (au + bv) = a(x + u) + b(y + v) \in S 이다.

  1. SS는 상수배 곱셈에 대해 닫혀있다. SS의 임의의 원소 ax+byax + by와 임의의 정수 dd에 대해

d(ax+by)=a(dx)+b(dy)Sd(ax + by) = a(dx) + b(dy) \in S 이다.

이러한 성질에 의해, 임의의 kk에 대해 rk2,rk1r_{k - 2}, r_{k - 1}SS에 포함된다면 rk=rk2qkrk1r_k = r_{k - 2} - q_k r_{k - 1}또한 SS에 포함됨을 알 수 있다. r1,r0Sr_{-1}, r_0 \in S이므로 수학적 귀납법을 통해 rn=(a,b)Sr_n = (a, b) \in S임을 알 수 있다.

한편, 디오판토스 방정식 ax+by=(a,b)ax + by = (a, b)에는 항상 정수해 x,yx, y가 존재한다는 정리를 베주 항등식Bézout's identity으로 부르기도 한다.

유클리드 알고리즘과 디오판토스 방정식 ax+by=(a,b)ax + by = (a, b)

디오판토스 방정식 ax+by=(a,b)ax + by = (a, b)는 정수해를 가짐을 보였다. 유클리드 알고리즘은 단지 이 방정식에서 정수해가 존재하는지 알려줄 뿐 만 아니라, 정수해를 계산할 수 있는 방법을 제시한다.

우선 a=r1,b=r0a = r_{-1}, b = r_0라고 하자. i=1,0,1,,ni = -1, 0, 1, \cdots, n에 대해 riSr_i \in S임을 보였으므로, asi+bti=rias_i + bt_i = r_i를 만족하는 정수 si,tis_i, t_i가 존재할 것이다. rk=rk2rk1qkr_k = r_{k - 2} - r_{k - 1} q_k를 관찰하자. sk2,sk1,tk2,tk1s_{k - 2}, s_{k - 1}, t_{k - 2}, t_{k - 1}으로 부터 sk,tks_k, t_k를 각각 다음과 같이 계산할 수 있다.

{sk=sk2sk1qktk=tk2tk1qk\begin{cases} s_k & = s_{k - 2} - s_{k - 1} q_k \\ t_k & = t_{k - 2} - t_{k - 1} q_k \end{cases}

r1=a1+b0,r0=a0+b1r_{-1} = a \cdot 1 + b \cdot 0, r_0 = a \cdot 0 + b \cdot 1은 쉽게 구할 수 있으므로, sns_ntnt_n을 총 nn번의 나눗셈을 통해 계산할 수 있다.

구현

기존에 구현하였던 유클리드 알고리즘에 sns_ntnt_n을 계산하는 기능을 추가하여 보자. 자료를 쉽게 다루기 위해서 다음고 같은 구조체를 정의한다.

struct eucPair {
    int x;
    int s, t;
};

x에는 rkr_k의 값이, s,ts, t에는 각각 rkr_ka,ba, b의 선형결합으로 나타내었을 때의 계수가 저장된다. 위에서 구한 점화식과 유클리드 알고리즘을 이용하여 디오판토스 방정식을 해결하는 알고리즘은 다음과 같다.

eucPair Euclidean(eucPair a, eucPair b) {
    if (b.x == 0) return a;
    int q = a.x / b.x;
    eucPair r;
    r.x = a.x % b.x;
    r.s = a.s - b.s * q;
    r.t = a.t - b.t * q;
    return Euclidean(b, r);
}

다시 꼬리 재귀함수가 나왔다.

활용

PS 또는 정수론 문제들을 풀게 된다면 이러한 형태의 디오판토스 방정식의 해를 구해야 하는 일이 종종 생긴다. 주어진 정수 aa에 대해, ax1(modn)ax \equiv 1 \pmod{n}을 만족하는 정수 xx를 법 nn에 대한 aa의 잉여역수라고 하고, x=a1x = a^{-1}으로 표기한다. (Z/nZ)×(\mathbb{Z}/n\mathbb{Z})^\times에서 xxaa의 역원이기 때문에 이러한 표기법을 사용한다. 이러한 xx를 찾는 문제는 이변수 선형 디오판토스 방정식으로 변형될 수 있다. ax+ny=1ax + ny = 1을 만족하는 정수 x,yx, y를 찾는 문제와 같기 때문이다. 확장된 유클리드 알고리즘을 사용하면 aa의 잉여역수를 O(logn)\mathcal{O}(\log n)의 시간복잡도로 찾을 수 있다. 물론, (a,n)>1(a, n) > 1이라면 잉여역수를 찾을 수 없을 것이다.